diff --git a/.github/workflows/stagehand-codemode.yml b/.github/workflows/stagehand-codemode.yml index 68c9ce7..cec967f 100644 --- a/.github/workflows/stagehand-codemode.yml +++ b/.github/workflows/stagehand-codemode.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - '.github/workflows/stagehand-codemode.yml' + - 'examples/integrations/crewai/stagehand-codemode/**' - 'examples/integrations/mastra/stagehand-codemode/**' - 'examples/integrations/vercel/stagehand-codemode/**' - 'packages/stagehand-codemode/**' @@ -12,6 +13,7 @@ on: branches: [main] paths: - '.github/workflows/stagehand-codemode.yml' + - 'examples/integrations/crewai/stagehand-codemode/**' - 'examples/integrations/mastra/stagehand-codemode/**' - 'examples/integrations/vercel/stagehand-codemode/**' - 'packages/stagehand-codemode/**' @@ -70,3 +72,25 @@ jobs: - run: pnpm --filter @browserbasehq/stagehand-codemode run build - run: pnpm --filter stagehand-codemode-mastra-example run check - run: pnpm --filter stagehand-codemode-mastra-example run smoke + + crewai: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10.9.0 + - uses: actions/setup-node@v4 + with: + node-version: 24.x + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + cache-dependency-path: examples/integrations/crewai/stagehand-codemode/requirements.txt + + - run: pnpm install --no-frozen-lockfile + - run: pnpm --filter @browserbasehq/stagehand-codemode run build + - run: python -m pip install -r examples/integrations/crewai/stagehand-codemode/requirements.txt + - run: python examples/integrations/crewai/stagehand-codemode/smoke.py diff --git a/examples/integrations/crewai/stagehand-codemode/README.md b/examples/integrations/crewai/stagehand-codemode/README.md new file mode 100644 index 0000000..8a956d1 --- /dev/null +++ b/examples/integrations/crewai/stagehand-codemode/README.md @@ -0,0 +1,10 @@ +# CrewAI + Stagehand code mode + +CrewAI's native `MCPServerStdio` launches the local MCP beside the agent. One server configuration is +attached to the `Agent` for the whole `kickoff` run, allowing repeated `code_execute` calls to reuse +the same browser. CrewAI cleans up the MCP client after agent execution. + +The shared Stagehand V4 syntax skill is placed in the agent's `backstory` and is also included in the +tool description discovered from MCP. + +See [`agent.py`](./agent.py). diff --git a/examples/integrations/crewai/stagehand-codemode/agent.py b/examples/integrations/crewai/stagehand-codemode/agent.py new file mode 100644 index 0000000..17e63cf --- /dev/null +++ b/examples/integrations/crewai/stagehand-codemode/agent.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +from crewai import Agent +from crewai.mcp import MCPServerStdio + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[4] +STDIO_SERVER_PATH = REPOSITORY_ROOT / "packages/stagehand-codemode/dist/stdio-server.js" +SKILL_PATH = REPOSITORY_ROOT / "packages/stagehand-codemode/SKILL.md" +STAGEHAND_CODEMODE_SKILL = SKILL_PATH.read_text().strip() + + +def build_stagehand_agent(llm: str | Any = "openai/gpt-5-mini") -> Agent: + server = MCPServerStdio( + command="node", + args=[str(STDIO_SERVER_PATH)], + env=dict(os.environ), + tool_filter=lambda _context, tool: tool.get("name") == "code_execute", + ) + return Agent( + role="Stagehand browser agent", + goal="Complete browser tasks by writing compact, correct Stagehand V4 JavaScript.", + backstory=STAGEHAND_CODEMODE_SKILL, + llm=llm, + mcps=[server], + max_iter=8, + verbose=False, + ) + + +def run_stagehand_agent(prompt: str, llm: str | Any = "openai/gpt-5-mini") -> str: + # CrewAI keeps this stdio server connected for the Agent.kickoff run and cleans + # its MCP clients when agent execution finishes. + return str(build_stagehand_agent(llm).kickoff(prompt)) diff --git a/examples/integrations/crewai/stagehand-codemode/requirements.txt b/examples/integrations/crewai/stagehand-codemode/requirements.txt new file mode 100644 index 0000000..e7bc384 --- /dev/null +++ b/examples/integrations/crewai/stagehand-codemode/requirements.txt @@ -0,0 +1,2 @@ +crewai>=1.15.9,<2 +crewai-tools[mcp]>=1.15.9,<2 diff --git a/examples/integrations/crewai/stagehand-codemode/smoke.py b/examples/integrations/crewai/stagehand-codemode/smoke.py new file mode 100644 index 0000000..fa4a44a --- /dev/null +++ b/examples/integrations/crewai/stagehand-codemode/smoke.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import os + +os.environ.setdefault("OPENAI_API_KEY", "discovery-only-placeholder") + +from agent import STAGEHAND_CODEMODE_SKILL, build_stagehand_agent # noqa: E402 + + +def main() -> None: + agent = build_stagehand_agent("openai/gpt-4o-mini") + try: + tools = agent.get_mcp_tools(agent.mcps or []) + names = [getattr(tool, "original_tool_name", tool.name) for tool in tools] + assert names == ["code_execute"] + assert "Stagehand V4 code-mode syntax" in tools[0].description + assert "stagehand.extract" in STAGEHAND_CODEMODE_SKILL + print("CrewAI MCPServerStdio discovery PASS: code_execute") + finally: + agent._cleanup_mcp_clients() + + +if __name__ == "__main__": + main()