Skip to content

Commit 98e212b

Browse files
authored
add example for custom tools and docs (#1211)
# why We didn't have docs for passing custom tools to stagehand agent or examples, only MCP integrations. Under the hood, stagehand treats both the same, and passing custom tools directly is (most times) more convenient and performant. # what changed Added an example `agent-custom-tools.ts` as well as docs under the basics > agent section # test plan
1 parent 9e95add commit 98e212b

File tree

4 files changed

+267
-13
lines changed

4 files changed

+267
-13
lines changed

.changeset/sharp-laws-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
Add an example for passing custom tools to agent
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* This example shows how to pass custom tools to stagehand agent (both CUA and non-CUA)
3+
*/
4+
import { z } from "zod/v3";
5+
import { tool } from "ai";
6+
import { Stagehand } from "../lib/v3";
7+
import chalk from "chalk";
8+
9+
// Mock weather API, replace with your own API/tool logic
10+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
11+
const fetchWeatherAPI = async (location: string) => {
12+
return {
13+
temp: 70,
14+
conditions: "sunny",
15+
};
16+
};
17+
18+
// Define the tool in an AI SDK format
19+
const getWeather = tool({
20+
description: "Get the current weather in a location",
21+
inputSchema: z.object({
22+
location: z.string().describe("The location to get weather for"),
23+
}),
24+
execute: async ({ location }) => {
25+
// Your custom logic here
26+
const weather = await fetchWeatherAPI(location);
27+
return {
28+
location,
29+
temperature: weather.temp,
30+
conditions: weather.conditions,
31+
};
32+
},
33+
});
34+
35+
async function main() {
36+
console.log(
37+
`\n${chalk.bold("Stagehand 🤘 Computer Use Agent (CUA) Demo")}\n`,
38+
);
39+
40+
// Initialize Stagehand
41+
const stagehand = new Stagehand({
42+
env: "LOCAL",
43+
verbose: 2,
44+
experimental: true, // You must enable experimental mode to use custom tools / MCP integrations
45+
model: "anthropic/claude-sonnet-4-5",
46+
});
47+
await stagehand.init();
48+
49+
try {
50+
const page = stagehand.context.pages()[0];
51+
52+
// Create a computer use agent
53+
const agent = stagehand.agent({
54+
cua: true,
55+
model: {
56+
modelName: "anthropic/claude-sonnet-4-5-20250929",
57+
apiKey: process.env.ANTHROPIC_API_KEY,
58+
},
59+
systemPrompt: `You are a helpful assistant that can use a web browser.
60+
You are currently on the following page: ${page.url()}.
61+
Do not ask follow up questions, the user will trust your judgement. Today's date is ${new Date().toLocaleDateString()}.`,
62+
tools: {
63+
getWeather, // Pass the tools to the agent
64+
},
65+
});
66+
67+
// const agent = stagehand.agent({
68+
// systemPrompt: `You are a helpful assistant that can use a web browser.
69+
// You are currently on the following page: ${page.url()}.
70+
// Do not ask follow up questions, the user will trust your judgement. Today's date is ${new Date().toLocaleDateString()}.`,
71+
// // Pass the tools to the agent
72+
// tools: {
73+
// getWeather: getWeather,
74+
// },
75+
// });
76+
77+
// Navigate to the Browserbase careers page
78+
await page.goto("https://www.google.com");
79+
80+
// Define the instruction for the CUA
81+
const instruction = "What's the weather in San Francisco?";
82+
console.log(`Instruction: ${chalk.white(instruction)}`);
83+
84+
// Execute the instruction
85+
const result = await agent.execute({
86+
instruction,
87+
maxSteps: 20,
88+
});
89+
90+
console.log(`${chalk.green("✓")} Execution complete`);
91+
console.log(`${chalk.yellow("⤷")} Result:`);
92+
console.log(chalk.white(JSON.stringify(result, null, 2)));
93+
} catch (error) {
94+
console.log(`${chalk.red("✗")} Error: ${error}`);
95+
if (error instanceof Error && error.stack) {
96+
console.log(chalk.dim(error.stack.split("\n").slice(1).join("\n")));
97+
}
98+
} finally {
99+
// Close the browser
100+
await stagehand.close();
101+
}
102+
}
103+
104+
main().catch((error) => {
105+
console.log(`${chalk.red("✗")} Unhandled error in main function`);
106+
console.log(chalk.red(error));
107+
});

packages/core/lib/v3/v3.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,25 +1363,34 @@ export class V3 {
13631363
} {
13641364
this.logger({
13651365
category: "agent",
1366-
message: "Creating v3 agent instance with options:",
1366+
message: `Creating v3 agent instance with options: ${JSON.stringify(options)}`,
13671367
level: 1,
13681368
auxiliary: {
13691369
cua: { value: options?.cua ? "true" : "false", type: "boolean" },
1370-
model:
1371-
typeof options?.model === "string"
1370+
model: options?.model
1371+
? typeof options?.model === "string"
13721372
? { value: options.model, type: "string" }
1373-
: { value: options.model.modelName, type: "string" },
1373+
: { value: options.model.modelName, type: "string" }
1374+
: { value: this.llmClient.modelName, type: "string" },
13741375
systemPrompt: { value: options?.systemPrompt ?? "", type: "string" },
13751376
tools: { value: JSON.stringify(options?.tools ?? {}), type: "object" },
1376-
integrations: {
1377-
value: JSON.stringify(options?.integrations ?? []),
1378-
type: "object",
1379-
},
1377+
...(options?.integrations && {
1378+
integrations: {
1379+
value: JSON.stringify(options.integrations),
1380+
type: "object",
1381+
},
1382+
}),
13801383
},
13811384
});
13821385

13831386
// If CUA is enabled, use the computer-use agent path
13841387
if (options?.cua) {
1388+
if ((options?.integrations || options?.tools) && !this.experimental) {
1389+
throw new Error(
1390+
"MCP integrations and custom tools are experimental. Enable experimental: true in V3 options.",
1391+
);
1392+
}
1393+
13851394
const modelToUse = options?.model || {
13861395
modelName: this.modelName,
13871396
...this.modelClientOptions,
@@ -1499,9 +1508,9 @@ export class V3 {
14991508
return {
15001509
execute: async (instructionOrOptions: string | AgentExecuteOptions) =>
15011510
withInstanceLogContext(this.instanceId, async () => {
1502-
if (options?.integrations && !this.experimental) {
1511+
if ((options?.integrations || options?.tools) && !this.experimental) {
15031512
throw new Error(
1504-
"MCP integrations are experimental. Enable experimental: true in V3 options.",
1513+
"MCP integrations and custom tools are experimental. Enable experimental: true in V3 options.",
15051514
);
15061515
}
15071516

packages/docs/v3/basics/agent.mdx

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,139 @@ When you use `agent()`, Stagehand will return a `Promise<AgentResult>` with the
142142
}
143143
```
144144

145+
## Custom Tools
146+
147+
Agents can be enhanced with custom tools for more granular control and better performance. Unlike MCP integrations, custom tools are defined inline and execute directly within your application.
148+
149+
<Note>Custom tools provide a cleaner, more performant alternative to MCP integrations when you need specific functionality.</Note>
150+
151+
### Defining Custom Tools
152+
153+
Use the `tool` helper from the [Vercel AI SDK](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) to define custom tools:
154+
155+
<CodeGroup>
156+
```typescript Basic Tool
157+
import { tool } from "ai";
158+
import { z } from "zod/v3";
159+
160+
const agent = stagehand.agent({
161+
model: "openai/gpt-5",
162+
tools: {
163+
getWeather: tool({
164+
description: 'Get the current weather in a location',
165+
inputSchema: z.object({
166+
location: z.string().describe('The location to get weather for'),
167+
}),
168+
execute: async ({ location }) => {
169+
// Your custom logic here
170+
const weather = await fetchWeatherAPI(location);
171+
return {
172+
location,
173+
temperature: weather.temp,
174+
conditions: weather.conditions,
175+
};
176+
},
177+
}),
178+
},
179+
systemPrompt: 'You are a helpful assistant with access to weather data.',
180+
});
181+
182+
await agent.execute("What's the weather in San Francisco and should I bring an umbrella?");
183+
```
184+
185+
```typescript Multiple Tools
186+
import { tool } from "ai";
187+
import { z } from "zod/v3";
188+
189+
const agent = stagehand.agent({
190+
cua: true,
191+
model: "anthropic/claude-sonnet-4-20250514",
192+
tools: {
193+
searchDatabase: tool({
194+
description: 'Search for records in the database',
195+
inputSchema: z.object({
196+
query: z.string().describe('The search query'),
197+
limit: z.number().optional().describe('Max results to return'),
198+
}),
199+
execute: async ({ query, limit = 10 }) => {
200+
const results = await db.search(query, limit);
201+
return { results };
202+
},
203+
}),
204+
205+
calculatePrice: tool({
206+
description: 'Calculate the total price with tax',
207+
inputSchema: z.object({
208+
amount: z.number().describe('The base amount'),
209+
taxRate: z.number().describe('Tax rate as decimal (e.g., 0.08 for 8%)'),
210+
}),
211+
execute: async ({ amount, taxRate }) => {
212+
const total = amount * (1 + taxRate);
213+
return { total: total.toFixed(2) };
214+
},
215+
}),
216+
},
217+
});
218+
219+
await agent.execute("Find products under $50 and calculate the total with 8% tax");
220+
```
221+
222+
```typescript Tool with API Integration
223+
import { tool } from "ai";
224+
import { z } from "zod/v3";
225+
226+
const agent = stagehand.agent({
227+
model: "google/gemini-2.0-flash",
228+
tools: {
229+
sendEmail: tool({
230+
description: 'Send an email via SendGrid',
231+
inputSchema: z.object({
232+
to: z.string().email().describe('Recipient email address'),
233+
subject: z.string().describe('Email subject'),
234+
body: z.string().describe('Email body content'),
235+
}),
236+
execute: async ({ to, subject, body }) => {
237+
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
238+
method: 'POST',
239+
headers: {
240+
'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}`,
241+
'Content-Type': 'application/json',
242+
},
243+
body: JSON.stringify({
244+
personalizations: [{ to: [{ email: to }] }],
245+
from: { email: '[email protected]' },
246+
subject,
247+
content: [{ type: 'text/plain', value: body }],
248+
}),
249+
});
250+
251+
return {
252+
sent: response.ok,
253+
messageId: response.headers.get('X-Message-Id'),
254+
};
255+
},
256+
}),
257+
},
258+
});
259+
260+
await agent.execute("Fill out the contact form and send me a confirmation email at [email protected]");
261+
```
262+
</CodeGroup>
263+
264+
### Custom Tools vs MCP Integrations
265+
266+
| Custom Tools | MCP Integrations |
267+
|----------------------------------------|-----------------------------------------|
268+
| Defined inline with your code | Connect to external services |
269+
| Direct function execution | Standard protocol |
270+
| Better performance & optimized context | Reusable across applications |
271+
| Type-safe with TypeScript | Access to pre-built integrations |
272+
| Granular control | Network-based communication |
273+
274+
<Tip>
275+
Use custom tools when you need specific functionality within your application. Use MCP integrations when connecting to external services or when you need standardized cross-application tools.
276+
</Tip>
277+
145278
## MCP Integrations
146279

147280
Agents can be enhanced with external tools and services through MCP (Model Context Protocol) integrations. This allows your agent to access external APIs and data sources beyond just browser interactions.
@@ -188,12 +321,12 @@ await agent.execute("Search for restaurants and save the first result to the dat
188321
MCP integrations enable agents to be more powerful by combining browser automation with external APIs, databases, and services. The agent can intelligently decide when to use browser actions versus external tools.
189322
</Tip>
190323

324+
## Agent Execution Configuration
325+
191326
<Warning>
192-
Stagehand uses a 1288x711 viewport by default (the optimal size for Computer Use Agents). Other viewport sizes may reduce performance. If you need to modify the viewport, you can edit in the [Browser Configuration](/v3/configuration/browser).
327+
Stagehand uses a 1288x711 viewport by default. Other viewport sizes may reduce performance. If you need to modify the viewport, you can edit in the [Browser Configuration](/v3/configuration/browser).
193328
</Warning>
194329

195-
## Agent Execution Configuration
196-
197330
Control the maximum number of steps the agent can take to complete the task using the `maxSteps` parameter.
198331

199332
<CodeGroup>

0 commit comments

Comments
 (0)