Skip to content

Commit bc88526

Browse files
authored
feat(integrations): Claude Agent SDK harness example (#2719)
Runnable project embedding a Claude agent via `@anthropic-ai/claude-agent-sdk`, with the `stagehand-facade` stdio server mounted programmatically — matching the pattern established by the evals `claude_code` harness (`query()` + `mcpServers`). - `allowedTools` restricted to `mcp__stagehand__{run,snapshot,screenshot}` plus a `canUseTool` guard (headless runs hang on unanswered permission prompts). - System prompt is `FACADE_AGENT_INSTRUCTIONS` imported from the facade package; server spawned with a `STAGEHAND_*`/`BROWSERBASE_*` env allowlist. - One-line run after install + core build: `pnpm --filter @browserbasehq/stagehand-integrations-example-claude-code-facade start "your instruction"` - Secondary README section: connecting a running Claude Code CLI via the project-scoped `.mcp.json` (verified: the CLI inherits the shell env; `${VAR}` in .mcp.json is not expanded). Verified: contract + allowlist tests in CI; Browserbase smoke — *"The page heading is 'Example Domain', per snapshot element [0-19]"*. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds a runnable Claude agent harness using `@anthropic-ai/claude-agent-sdk` that mounts the Stagehand facade MCP stdio server. Provides a one-command example and an optional `.mcp.json` for the Claude Code CLI; only `STAGEHAND_*`/`BROWSERBASE_*` env reach the browser. - New package: `@browserbasehq/stagehand-integrations-example-claude-code-facade` (Node 24+). Run: `pnpm --filter @browserbasehq/stagehand-integrations-example-claude-code-facade start "your instruction"`. Choose model via `CLAUDE_STAGEHAND_MODEL` (default `claude-sonnet-5`). - Spawns `@browserbasehq/stagehand-integrations/facade/stdio-server` with an env allowlist; `ANTHROPIC_API_KEY` is never forwarded. - Restricts tools to `mcp__stagehand__{run,snapshot,screenshot}` via `allowedTools` and a deny-by-default `canUseTool`. Uses `FACADE_AGENT_INSTRUCTIONS` as the system prompt. - `.mcp.json` enables the `claude` CLI in this directory; the CLI inherits the shell env (note: `${VAR}` is not expanded in `.mcp.json`). - Tests cover the tool name contract and env allowlist. Robustness: resolve server path inside `main`, add `import.meta.main` guard, and exit nonzero if the agent stream ends without a result. Workspace updates add `@anthropic-ai/claude-agent-sdk` to the catalog/lockfile and a `turbo` typecheck target; no changes to other packages’ runtime behavior. <sup>Written for commit 129610f. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/browserbase/stagehand/pull/2719?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
1 parent 0740acf commit bc88526

11 files changed

Lines changed: 335 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"stagehand": {
4+
"command": "node",
5+
"args": ["../core/dist/facade/stdio-server.mjs"]
6+
}
7+
}
8+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Claude Agent SDK + Stagehand facade over MCP/stdio
2+
3+
A runnable example embedding a Claude agent via `@anthropic-ai/claude-agent-sdk`, with the
4+
Stagehand facade (`run` / `snapshot` / `screenshot`) wired in as a stdio MCP server
5+
programmatically — install, export keys, one line to run.
6+
7+
## Setup
8+
9+
Use Node.js 24 or later. From the repository root, build the integrations package first:
10+
11+
```bash
12+
pnpm install
13+
pnpm exec turbo run build --filter @browserbasehq/stagehand-integrations
14+
```
15+
16+
Export credentials (Browserbase is the default and recommended backend):
17+
18+
```bash
19+
export ANTHROPIC_API_KEY=sk-ant-...
20+
export BROWSERBASE_API_KEY=bb_live_...
21+
export BROWSERBASE_PROJECT_ID=...
22+
```
23+
24+
## Run
25+
26+
```bash
27+
pnpm --filter @browserbasehq/stagehand-integrations-example-claude-code-facade start "Open https://example.com, snapshot it, and report the heading citing the snapshot ID."
28+
```
29+
30+
| Variable | Purpose |
31+
| ------------------------ | ------------------------------------------------------------------------------------------------ |
32+
| `STAGEHAND_BROWSER` | Browser backend. Defaults to `browserbase` when `BROWSERBASE_API_KEY` is set, otherwise `local`. |
33+
| `BROWSERBASE_API_KEY` | Browserbase credential for the browser session. |
34+
| `BROWSERBASE_PROJECT_ID` | Optional Browserbase project. |
35+
| `CLAUDE_STAGEHAND_MODEL` | Agent model; defaults to `claude-sonnet-5`. |
36+
| `ANTHROPIC_API_KEY` | Claude Agent SDK credential (never forwarded to the browser). |
37+
38+
The agent is restricted to the three `mcp__stagehand__*` tools (`allowedTools` plus a
39+
`canUseTool` guard — headless runs hang on unanswered permission prompts otherwise), and its
40+
system prompt is the canonical `FACADE_AGENT_INSTRUCTIONS` imported from the facade package.
41+
42+
## Connecting a running Claude Code CLI instead
43+
44+
To use the facade from the interactive `claude` CLI rather than the SDK, the project-scoped
45+
`.mcp.json` in this directory is all that's needed — Claude Code inherits your shell
46+
environment, so the exports above are the only configuration:
47+
48+
```bash
49+
cd packages/integrations/claude-code
50+
claude
51+
```
52+
53+
Headless one-shot form:
54+
55+
```bash
56+
claude -p "your instruction" --mcp-config .mcp.json --allowedTools "mcp__stagehand__run,mcp__stagehand__snapshot,mcp__stagehand__screenshot"
57+
```
58+
59+
## Security model
60+
61+
The `run` tool executes model-authored JavaScript inside the Stagehand browser extension's
62+
service worker — browser-side, never on your machine. Browserbase is the recommended isolation
63+
boundary: the privileged execution environment is a disposable cloud browser. The SDK example
64+
spawns the facade server with an explicit `STAGEHAND_*`/`BROWSERBASE_*` allowlist; your
65+
Anthropic credentials never reach the browser session.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@browserbasehq/stagehand-integrations-example-claude-code-facade",
3+
"version": "4.0.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"start": "node src/agent.ts",
8+
"test": "pnpm -w exec turbo run build --filter @browserbasehq/stagehand-integrations && vitest run",
9+
"test:unit": "vitest run",
10+
"typecheck": "tsc --noEmit"
11+
},
12+
"dependencies": {
13+
"@anthropic-ai/claude-agent-sdk": "catalog:",
14+
"@browserbasehq/stagehand-integrations": "workspace:*"
15+
},
16+
"devDependencies": {
17+
"@types/node": "catalog:",
18+
"typescript": "catalog:",
19+
"vitest": "catalog:"
20+
},
21+
"engines": {
22+
"node": ">=24"
23+
}
24+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { query } from "@anthropic-ai/claude-agent-sdk";
2+
import {
3+
FACADE_AGENT_INSTRUCTIONS,
4+
FACADE_TOOLS,
5+
} from "@browserbasehq/stagehand-integrations/facade";
6+
import { fileURLToPath } from "node:url";
7+
8+
import { buildAllowlistedEnv } from "./env.ts";
9+
10+
export const STAGEHAND_TOOL_NAMES = FACADE_TOOLS.map((tool) => `mcp__stagehand__${tool.name}`);
11+
12+
async function main(): Promise<void> {
13+
const args = process.argv.slice(2);
14+
const instruction = (args[0] === "--" ? args.slice(1) : args).join(" ").trim();
15+
if (!instruction) throw new Error('Usage: pnpm start "your instruction"');
16+
17+
// Resolved here (not at module load) so a missing build surfaces through
18+
// handleFailure instead of an uncaught module error.
19+
const serverPath = fileURLToPath(
20+
import.meta.resolve("@browserbasehq/stagehand-integrations/facade/stdio-server"),
21+
);
22+
23+
const result = query({
24+
prompt: instruction,
25+
options: {
26+
model: process.env.CLAUDE_STAGEHAND_MODEL ?? "claude-sonnet-5",
27+
maxTurns: 20,
28+
systemPrompt: FACADE_AGENT_INSTRUCTIONS,
29+
mcpServers: {
30+
stagehand: {
31+
command: process.execPath,
32+
args: [serverPath],
33+
env: buildAllowlistedEnv(),
34+
},
35+
},
36+
allowedTools: STAGEHAND_TOOL_NAMES,
37+
// Headless runs hang on any unanswered permission prompt; allow exactly
38+
// the stagehand tools and deny everything else.
39+
canUseTool: async (toolName) =>
40+
toolName.startsWith("mcp__stagehand__")
41+
? { behavior: "allow", updatedInput: undefined }
42+
: { behavior: "deny", message: "Only stagehand browser tools are permitted." },
43+
},
44+
});
45+
46+
for await (const message of result) {
47+
if (message.type === "result") {
48+
if (message.subtype === "success") {
49+
// oxlint-disable-next-line no-console -- CLI example prints the agent result.
50+
console.log(message.result);
51+
return;
52+
}
53+
throw new Error(`Agent did not finish: ${message.subtype}`);
54+
}
55+
}
56+
throw new Error("Agent stream ended without a result message.");
57+
}
58+
59+
if (import.meta.main) {
60+
main().catch(handleFailure);
61+
}
62+
63+
function handleFailure(error: unknown): void {
64+
// oxlint-disable-next-line no-console -- CLI example reports failures to stderr.
65+
console.error(error instanceof Error ? error.message : error);
66+
process.exitCode = 1;
67+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/** Only STAGEHAND_* and BROWSERBASE_* host env vars cross into the server. */
2+
export function buildAllowlistedEnv(): Record<string, string> {
3+
const env: Record<string, string> = {};
4+
for (const [key, value] of Object.entries(process.env)) {
5+
if (/^(STAGEHAND_|BROWSERBASE_)/.test(key) && value) {
6+
env[key] = value;
7+
}
8+
}
9+
return env;
10+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { FACADE_TOOLS } from "@browserbasehq/stagehand-integrations/facade";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { STAGEHAND_TOOL_NAMES } from "../src/agent.ts";
5+
import { buildAllowlistedEnv } from "../src/env.ts";
6+
7+
describe("claude-code stagehand example", () => {
8+
it("allows exactly the namespaced facade tools", () => {
9+
const expected = FACADE_TOOLS.map((tool) => `mcp__stagehand__${tool.name}`).sort();
10+
expect([...STAGEHAND_TOOL_NAMES].sort()).toEqual(expected);
11+
});
12+
13+
it("allowlist excludes non-STAGEHAND/BROWSERBASE host vars", () => {
14+
process.env.STAGEHAND_BROWSER = "local";
15+
process.env.NOT_ALLOWLISTED_SECRET = "must-not-cross";
16+
try {
17+
const env = buildAllowlistedEnv();
18+
expect(env.STAGEHAND_BROWSER).toBe("local");
19+
expect(env).not.toHaveProperty("NOT_ALLOWLISTED_SECRET");
20+
} finally {
21+
delete process.env.STAGEHAND_BROWSER;
22+
delete process.env.NOT_ALLOWLISTED_SECRET;
23+
}
24+
});
25+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "../../../tsconfig.json",
3+
"compilerOptions": {
4+
"noEmit": true,
5+
"module": "nodenext",
6+
"moduleResolution": "nodenext",
7+
"skipLibCheck": true
8+
},
9+
"include": ["src/**/*.ts", "tests/**/*.ts", "vitest.config.ts"]
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
include: ["tests/**/*.test.ts"],
6+
hookTimeout: 20_000,
7+
testTimeout: 20_000,
8+
},
9+
});

0 commit comments

Comments
 (0)