Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c2ed35d
feat(langchain): add Stagehand code-mode MCP example
shrey150 Aug 6, 2026
d57bce1
Merge updated CrewAI integration parent
shrey150 Aug 6, 2026
9c819cd
Merge updated CrewAI integration parent
shrey150 Aug 7, 2026
c9fa671
Merge updated CrewAI integration parent
shrey150 Aug 7, 2026
ced3e2c
Merge refreshed CrewAI integration parent
shrey150 Aug 7, 2026
3f6107f
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-crewai' …
shrey150 Aug 8, 2026
8d9cc5f
docs: sandbox the Deep Agents code-mode MCP
shrey150 Aug 8, 2026
2e20aa4
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-crewai' …
shrey150 Aug 8, 2026
0c89f77
fix: preserve CI Chrome launch settings
shrey150 Aug 8, 2026
da13c9b
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-crewai' …
shrey150 Aug 8, 2026
a16cb8a
fix: filter Deep Agents bridge overrides
shrey150 Aug 8, 2026
2a6d8cc
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-crewai' …
shrey150 Aug 8, 2026
0b5bccf
refactor(langchain): use sandboxed code-mode MCP
shrey150 Aug 8, 2026
095b04b
Merge latest CrewAI parent into LangChain
shrey150 Aug 8, 2026
0d5c95a
fix(langchain): gate live proof on credentials
shrey150 Aug 8, 2026
e6cff47
Merge latest CrewAI review fixes into LangChain
shrey150 Aug 8, 2026
c29ce4a
fix(langchain): harden session and agent contracts
shrey150 Aug 8, 2026
2fe3165
Merge latest CrewAI review fixes into LangChain
shrey150 Aug 8, 2026
fddca9e
Merge latest CrewAI contract tests into LangChain
shrey150 Aug 8, 2026
a05413b
fix(langchain): classify session cleanup failures
shrey150 Aug 8, 2026
dde6fd4
Merge latest CrewAI lifecycle fixes into LangChain
shrey150 Aug 8, 2026
1dbce0b
Merge deterministic CrewAI stack test into LangChain
shrey150 Aug 8, 2026
c91a195
Merge latest CrewAI review fixes into LangChain
shrey150 Aug 8, 2026
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
28 changes: 28 additions & 0 deletions .github/workflows/codemode-framework-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,31 @@ jobs:
env:
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
STAGEHAND_BROWSER: local

langchain:
name: LangChain Deep Agents
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

- uses: ./.github/actions/setup-node-pnpm
with:
use-prebuilt-artifacts: "false"

- uses: ./.github/actions/setup-chrome-verified
id: setup-chrome

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: packages/integrations/examples/langchain/requirements.txt

- run: pnpm exec turbo run build --filter @browserbasehq/stagehand-integrations
- run: python -m pip install -r packages/integrations/examples/langchain/requirements.txt
- run: python packages/integrations/examples/langchain/smoke.py
env:
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
STAGEHAND_BROWSER: local
1 change: 1 addition & 0 deletions packages/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The process stays alive across calls and closes when its input stream ends. `SIG
- [Vercel AI SDK](./examples/vercel) launches the stdio server through the AI SDK MCP client and keeps one process alive for the complete agent run.
- [Mastra](./examples/mastra) discovers the canonical MCP toolset once and reuses one client and browser for the complete agent run.
- [CrewAI](./examples/crewai) keeps its context-managed MCP adapter open across every tool call in one crew execution.
- [LangChain Deep Agents](./examples/langchain) uses one explicit MCP session so every tool call reaches the same browser.

### Configuration

Expand Down
50 changes: 50 additions & 0 deletions packages/integrations/examples/langchain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# LangChain Deep Agents + Stagehand code mode

This example connects LangChain Deep Agents to the canonical Stagehand code-mode MCP server. The
server runs as a local stdio child process and exposes exactly one tool, `code_execute`.

The explicit `client.session("stagehand")` context in [`agent.py`](./agent.py) is required. Do not
replace it with `client.get_tools()`: that convenience API creates a new session for each tool call,
which would start a new stdio process and lose the browser state created by the previous call.

## Setup

Build the Stagehand-local MCP server from the repository root:

```bash
pnpm exec turbo run build --filter @browserbasehq/stagehand-integrations
```

Create an isolated Python 3.12 environment from this directory:

```bash
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -r requirements.txt
```

## Prove persistent local-browser state

```bash
STAGEHAND_BROWSER=local .venv/bin/python smoke.py
```

The smoke launches the compiled MCP server through LangChain, discovers exactly `code_execute`,
then invokes it twice inside one explicit MCP session. The first call opens `example.com` and writes
a DOM marker; the second call proves that the same page and marker are still present.

## Run a Deep Agent

Provider credentials and Stagehand configuration are inherited by the MCP child. For example:

```bash
export OPENAI_API_KEY=<your-provider-key>
export STAGEHAND_BROWSER=local
.venv/bin/python agent.py "Open example.com and return its heading and title."
```

To use Browserbase instead, set `STAGEHAND_BROWSER=browserbase`, `BROWSERBASE_API_KEY`, and
optionally `BROWSERBASE_PROJECT_ID`. You can select another supported model with
`STAGEHAND_LANGCHAIN_MODEL`; it defaults to `openai:gpt-5-mini` for the Deep Agent.

The canonical Stagehand V4 syntax guide from `packages/integrations/codemode/SKILL.md` is loaded as
the agent system prompt. This example does not copy the executor, schema, skill, or runtime.
97 changes: 97 additions & 0 deletions packages/integrations/examples/langchain/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from __future__ import annotations

import asyncio
import os
import sys
from collections.abc import Mapping
from pathlib import Path
from typing import Any

from deepagents import create_deep_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools
from mcp import ClientSession

REPOSITORY_ROOT = Path(__file__).resolve().parents[4]
STDIO_SERVER_PATH = (
REPOSITORY_ROOT / "packages/integrations/dist/codemode/stdio-server.mjs"
)
SKILL_PATH = REPOSITORY_ROOT / "packages/integrations/codemode/SKILL.md"
STAGEHAND_CODEMODE_SKILL = SKILL_PATH.read_text(encoding="utf-8").strip()
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated


def create_stagehand_mcp_client(
env: Mapping[str, str] | None = None,
) -> MultiServerMCPClient:
"""Create a client that forwards Stagehand local/Browserbase configuration."""
if not STDIO_SERVER_PATH.is_file():
raise RuntimeError(
"Build the Stagehand integrations package before running this example: "
"pnpm exec turbo run build --filter @browserbasehq/stagehand-integrations"
)

child_env = dict(os.environ if env is None else env)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
return MultiServerMCPClient(
{
"stagehand": {
"transport": "stdio",
"command": "node",
"args": [str(STDIO_SERVER_PATH)],
"env": child_env,
}
},
tool_name_prefix=False,
handle_tool_errors=True,
)


async def load_stagehand_code_tool(session: ClientSession) -> Any:
"""Discover the one canonical tool without creating another MCP session."""
tools = await load_mcp_tools(session)
tool_names = [tool.name for tool in tools]
if tool_names != ["code_execute"]:
raise RuntimeError(f"Expected exactly code_execute, got {tool_names}")
return tools[0]


async def run_stagehand_agent(
prompt: str,
model: str | Any = "openai:gpt-5-mini",
) -> dict[str, Any]:
client = create_stagehand_mcp_client()

# Keep discovery, agent construction, and the complete invocation inside one
# explicit session. A convenience get_tools() call would create fresh stdio
# sessions and discard Stagehand browser state between tool calls.
async with client.session("stagehand") as session:
code_tool = await load_stagehand_code_tool(session)
agent = create_deep_agent(
model=model,
tools=[code_tool],
system_prompt=STAGEHAND_CODEMODE_SKILL,
)
return await agent.ainvoke(
{"messages": [{"role": "user", "content": prompt}]},
config={"recursion_limit": 20},
)


def _last_message_text(result: dict[str, Any]) -> str:
messages = result.get("messages", [])
if not messages:
return str(result)
content = getattr(messages[-1], "content", messages[-1])
return content if isinstance(content, str) else str(content)


async def _main() -> None:
if len(sys.argv) < 2:
raise SystemExit(f'Usage: {Path(sys.argv[0]).name} "<browser task>"')
prompt = " ".join(sys.argv[1:])
model = os.environ.get("STAGEHAND_LANGCHAIN_MODEL", "openai:gpt-5-mini")
result = await run_stagehand_agent(prompt, model=model)
print(_last_message_text(result))


if __name__ == "__main__":
asyncio.run(_main())
4 changes: 4 additions & 0 deletions packages/integrations/examples/langchain/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
deepagents>=0.7.1,<1
langchain-mcp-adapters>=0.3.1,<1
langchain-openai>=1.2.1,<2
mcp>=1.24.0,<2
94 changes: 94 additions & 0 deletions packages/integrations/examples/langchain/smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from __future__ import annotations

import asyncio
import json
import os
from typing import Any

from agent import create_stagehand_mcp_client, load_stagehand_code_tool

FIRST_CALL = """
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
await page.evaluate(() => {
document.documentElement.dataset.stagehandLangchainSmoke = "persistent";
});
return {
pageId: page.pageId,
title: await page.title(),
marker: await page.evaluate(
() => document.documentElement.dataset.stagehandLangchainSmoke ?? null,
),
};
"""

SECOND_CALL = """
return {
pageId: page.pageId,
title: await page.title(),
marker: await page.evaluate(
() => document.documentElement.dataset.stagehandLangchainSmoke ?? null,
),
};
"""


def parse_code_execute_result(raw_result: Any) -> dict[str, Any]:
"""Normalize the text result returned by the LangChain MCP adapter."""
if isinstance(raw_result, str):
parsed = json.loads(raw_result)
elif isinstance(raw_result, dict):
structured = raw_result.get("structuredContent")
parsed = structured if isinstance(structured, dict) else raw_result
elif isinstance(raw_result, list) and len(raw_result) == 1:
block = raw_result[0]
text = (
block.get("text")
if isinstance(block, dict)
else getattr(block, "text", None)
)
if not isinstance(text, str):
raise TypeError(f"Unexpected MCP content block: {type(block).__name__}")
parsed = json.loads(text)
else:
raise TypeError(f"Unexpected code_execute result: {type(raw_result).__name__}")

if not isinstance(parsed, dict):
raise TypeError(f"Expected an object result, got {type(parsed).__name__}")
return parsed


async def main() -> None:
child_env = dict(os.environ)
child_env.setdefault("STAGEHAND_BROWSER", "local")
client = create_stagehand_mcp_client(child_env)

async with client.session("stagehand") as session:
code_tool = await load_stagehand_code_tool(session)
first = parse_code_execute_result(await code_tool.ainvoke({"code": FIRST_CALL}))
second = parse_code_execute_result(
await code_tool.ainvoke({"code": SECOND_CALL})
)

assert first.get("ok") is True, first
assert second.get("ok") is True, second
first_value = first.get("value")
second_value = second.get("value")
assert isinstance(first_value, dict), first
assert isinstance(second_value, dict), second
assert first_value["marker"] == "persistent", first_value
assert second_value["marker"] == "persistent", second_value
assert first_value["pageId"] == second_value["pageId"], (first_value, second_value)
assert first_value["title"] == second_value["title"] == "Example Domain", (
first_value,
second_value,
)

print(
"LangChain persistent Stagehand session PASS: "
f"browser={child_env['STAGEHAND_BROWSER']}, code_execute -> code_execute, "
"same pageId, marker=persistent, title=Example Domain"
)


if __name__ == "__main__":
asyncio.run(main())