Skip to content

Commit 14ae539

Browse files
committed
rm legacy tool call and separate tools into individual files
1 parent a7d952a commit 14ae539

File tree

9 files changed

+403
-341
lines changed

9 files changed

+403
-341
lines changed

stagehand/src/context.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Stagehand } from "@browserbasehq/stagehand";
22
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
33
import type { Config } from "../config.js";
44
import { CallToolResult, TextContent, ImageContent } from "@modelcontextprotocol/sdk/types.js";
5-
import { handleToolCall } from "./tools/tools.js";
65
import { listResources, readResource } from "./resources.js";
76
import {
87
ensureLogDirectory,
@@ -172,10 +171,8 @@ export class Context {
172171
try {
173172
log(`Executing tool: ${tool.schema.name} with args: ${JSON.stringify(args)}`, 'info');
174173

175-
// Check if this tool has a handle method (new session tools)
176-
// Only use handle method for session create and close tools
177-
if ("handle" in tool && typeof tool.handle === "function" &&
178-
(tool.schema.name === "browserbase_session_create" || tool.schema.name === "browserbase_session_close")) {
174+
// Check if this tool has a handle method (new tool system)
175+
if ("handle" in tool && typeof tool.handle === "function") {
179176
const toolResult = await tool.handle(this as any, args);
180177

181178
if (toolResult?.action) {
@@ -193,10 +190,8 @@ export class Context {
193190
};
194191
}
195192
} else {
196-
const stagehand = await this.getStagehand();
197-
const result = await handleToolCall(tool.schema.name, args, stagehand);
198-
log(`Tool ${tool.schema.name} completed successfully`, 'info');
199-
return result;
193+
// Fallback for any legacy tools without handle method
194+
throw new Error(`Tool ${tool.schema.name} does not have a handle method`);
200195
}
201196
} catch (error) {
202197
const errorMessage = error instanceof Error ? error.message : String(error);

stagehand/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSche
88
import { z } from "zod";
99
import { zodToJsonSchema } from "zod-to-json-schema";
1010
import { Context } from "./context.js";
11-
import { TOOLS } from "./tools/tools.js";
11+
import { TOOLS } from "./tools/index.js";
1212

1313
export async function createServer(config: Config): Promise<Server> {
1414
// Create the server

stagehand/src/tools/act.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { z } from "zod";
2+
import type { Tool, ToolSchema, ToolResult } from "./tool.js";
3+
import type { Context } from "../context.js";
4+
import type { ToolActionResult } from "../context.js";
5+
6+
const ActInputSchema = z.object({
7+
action: z.string().describe(
8+
"The action to perform. Should be as atomic and specific as possible, " +
9+
"i.e. 'Click the sign in button' or 'Type 'hello' into the search input'. AVOID actions that are more than one " +
10+
"step, i.e. 'Order me pizza' or 'Send an email to Paul asking him to call me'. The instruction should be just as specific as possible, " +
11+
"and have a strong correlation to the text on the page. If unsure, use observe before using act."
12+
),
13+
variables: z.object({}).optional().describe(
14+
"Variables used in the action template. ONLY use variables if you're dealing " +
15+
"with sensitive data or dynamic content. For example, if you're logging in to a website, " +
16+
"you can use a variable for the password. When using variables, you MUST have the variable " +
17+
"key in the action template. For example: {\"action\": \"Fill in the password\", \"variables\": {\"password\": \"123456\"}}"
18+
),
19+
});
20+
21+
type ActInput = z.infer<typeof ActInputSchema>;
22+
23+
const actSchema: ToolSchema<typeof ActInputSchema> = {
24+
name: "stagehand_act",
25+
description:
26+
"Performs an action on a web page element. Act actions should be as atomic and " +
27+
"specific as possible, i.e. \"Click the sign in button\" or \"Type 'hello' into the search input\". " +
28+
"AVOID actions that are more than one step, i.e. \"Order me pizza\" or \"Send an email to Paul " +
29+
"asking him to call me\".",
30+
inputSchema: ActInputSchema,
31+
};
32+
33+
async function handleAct(
34+
context: Context,
35+
params: ActInput
36+
): Promise<ToolResult> {
37+
const action = async (): Promise<ToolActionResult> => {
38+
try {
39+
const stagehand = await context.getStagehand();
40+
await stagehand.page.act({
41+
action: params.action,
42+
variables: params.variables,
43+
});
44+
45+
return {
46+
content: [
47+
{
48+
type: "text",
49+
text: `Action performed: ${params.action}`,
50+
},
51+
],
52+
};
53+
} catch (error) {
54+
const errorMsg = error instanceof Error ? error.message : String(error);
55+
throw new Error(`Failed to perform action: ${errorMsg}`);
56+
}
57+
};
58+
59+
return {
60+
action,
61+
captureSnapshot: false,
62+
code: [],
63+
waitForNetwork: false,
64+
};
65+
}
66+
67+
const actTool: Tool<typeof ActInputSchema> = {
68+
capability: "core",
69+
schema: actSchema,
70+
handle: handleAct,
71+
};
72+
73+
export default actTool;

stagehand/src/tools/extract.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { z } from "zod";
2+
import type { Tool, ToolSchema, ToolResult } from "./tool.js";
3+
import type { Context } from "../context.js";
4+
import type { ToolActionResult } from "../context.js";
5+
6+
const ExtractInputSchema = z.object({
7+
random_string: z.string().optional().describe("Dummy parameter for no-parameter tools"),
8+
});
9+
10+
type ExtractInput = z.infer<typeof ExtractInputSchema>;
11+
12+
const extractSchema: ToolSchema<typeof ExtractInputSchema> = {
13+
name: "stagehand_extract",
14+
description: "Extracts all of the text from the current page.",
15+
inputSchema: ExtractInputSchema,
16+
};
17+
18+
async function handleExtract(
19+
context: Context,
20+
params: ExtractInput
21+
): Promise<ToolResult> {
22+
const action = async (): Promise<ToolActionResult> => {
23+
try {
24+
const page = await context.getActivePage();
25+
if (!page) {
26+
throw new Error("No active page available");
27+
}
28+
29+
const bodyText = await page.evaluate(() => document.body.innerText);
30+
const content = bodyText
31+
.split("\n")
32+
.map((line) => line.trim())
33+
.filter((line) => {
34+
if (!line) return false;
35+
36+
if (
37+
(line.includes("{") && line.includes("}")) ||
38+
line.includes("@keyframes") || // Remove CSS animations
39+
line.match(/^\.[a-zA-Z0-9_-]+\s*{/) || // Remove CSS lines starting with .className {
40+
line.match(/^[a-zA-Z-]+:[a-zA-Z0-9%\s\(\)\.,-]+;$/) // Remove lines like "color: blue;" or "margin: 10px;"
41+
) {
42+
return false;
43+
}
44+
return true;
45+
})
46+
.map((line) => {
47+
return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) =>
48+
String.fromCharCode(parseInt(hex, 16))
49+
);
50+
});
51+
52+
return {
53+
content: [
54+
{
55+
type: "text",
56+
text: `Extracted content:\n${content.join("\n")}`,
57+
},
58+
],
59+
};
60+
} catch (error) {
61+
const errorMsg = error instanceof Error ? error.message : String(error);
62+
throw new Error(`Failed to extract content: ${errorMsg}`);
63+
}
64+
};
65+
66+
return {
67+
action,
68+
captureSnapshot: false,
69+
code: [],
70+
waitForNetwork: false,
71+
};
72+
}
73+
74+
const extractTool: Tool<typeof ExtractInputSchema> = {
75+
capability: "core",
76+
schema: extractSchema,
77+
handle: handleExtract,
78+
};
79+
80+
export default extractTool;

stagehand/src/tools/index.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Import all individual tools
2+
import navigateTool from "./navigate.js";
3+
import actTool from "./act.js";
4+
import extractTool from "./extract.js";
5+
import observeTool from "./observe.js";
6+
import screenshotTool from "./screenshot.js";
7+
import sessionTools from "./session.js";
8+
9+
// Export individual tools
10+
export { default as navigateTool } from "./navigate.js";
11+
export { default as actTool } from "./act.js";
12+
export { default as extractTool } from "./extract.js";
13+
export { default as observeTool } from "./observe.js";
14+
export { default as screenshotTool } from "./screenshot.js";
15+
export { default as sessionTools } from "./session.js";
16+
17+
// Export all tools as array
18+
export const TOOLS = [
19+
...sessionTools,
20+
navigateTool,
21+
actTool,
22+
extractTool,
23+
observeTool,
24+
screenshotTool,
25+
];
26+
27+
// Export tools by category
28+
export const coreTools = [
29+
navigateTool,
30+
actTool,
31+
extractTool,
32+
observeTool,
33+
screenshotTool,
34+
];
35+
36+
export const sessionManagementTools = sessionTools;

stagehand/src/tools/navigate.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { z } from "zod";
2+
import type { Tool, ToolSchema, ToolResult } from "./tool.js";
3+
import type { Context } from "../context.js";
4+
import type { ToolActionResult } from "../context.js";
5+
import { operationLogs } from "../logging.js";
6+
7+
const NavigateInputSchema = z.object({
8+
url: z.string().describe("The URL to navigate to"),
9+
});
10+
11+
type NavigateInput = z.infer<typeof NavigateInputSchema>;
12+
13+
const navigateSchema: ToolSchema<typeof NavigateInputSchema> = {
14+
name: "stagehand_navigate",
15+
description:
16+
"Navigate to a URL in the browser. Only use this tool with URLs you're confident will work and stay up to date. Otheriwse use https://google.com as the starting point",
17+
inputSchema: NavigateInputSchema,
18+
};
19+
20+
async function handleNavigate(
21+
context: Context,
22+
params: NavigateInput
23+
): Promise<ToolResult> {
24+
const action = async (): Promise<ToolActionResult> => {
25+
try {
26+
const page = await context.getActivePage();
27+
if (!page) {
28+
throw new Error("No active page available");
29+
}
30+
await page.goto(params.url, { waitUntil: "domcontentloaded" });
31+
32+
return {
33+
content: [
34+
{
35+
type: "text",
36+
text: `Navigated to: ${params.url}`,
37+
},
38+
{
39+
type: "text",
40+
text: `View the live session here: https://browserbase.com/sessions/${context.currentSessionId}`,
41+
},
42+
],
43+
};
44+
} catch (error) {
45+
const errorMsg = error instanceof Error ? error.message : String(error);
46+
throw new Error(`Failed to navigate: ${errorMsg}`);
47+
}
48+
};
49+
50+
return {
51+
action,
52+
captureSnapshot: false,
53+
code: [],
54+
waitForNetwork: false,
55+
};
56+
}
57+
58+
const navigateTool: Tool<typeof NavigateInputSchema> = {
59+
capability: "core",
60+
schema: navigateSchema,
61+
handle: handleNavigate,
62+
};
63+
64+
export default navigateTool;

stagehand/src/tools/observe.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { z } from "zod";
2+
import type { Tool, ToolSchema, ToolResult } from "./tool.js";
3+
import type { Context } from "../context.js";
4+
import type { ToolActionResult } from "../context.js";
5+
6+
const ObserveInputSchema = z.object({
7+
instruction: z.string().describe(
8+
"Instruction for observation (e.g., 'find the login button'). This instruction must be extremely specific."
9+
),
10+
});
11+
12+
type ObserveInput = z.infer<typeof ObserveInputSchema>;
13+
14+
const observeSchema: ToolSchema<typeof ObserveInputSchema> = {
15+
name: "stagehand_observe",
16+
description:
17+
"Observes elements on the web page. Use this tool to observe elements that you can later use in an action. Use observe instead of extract when dealing with actionable (interactable) elements rather than text. More often than not, you'll want to use extract instead of observe when dealing with scraping or extracting structured text.",
18+
inputSchema: ObserveInputSchema,
19+
};
20+
21+
async function handleObserve(
22+
context: Context,
23+
params: ObserveInput
24+
): Promise<ToolResult> {
25+
const action = async (): Promise<ToolActionResult> => {
26+
try {
27+
const stagehand = await context.getStagehand();
28+
const observations = await stagehand.page.observe({
29+
instruction: params.instruction,
30+
returnAction: false,
31+
});
32+
33+
return {
34+
content: [
35+
{
36+
type: "text",
37+
text: `Observations: ${JSON.stringify(observations)}`,
38+
},
39+
],
40+
};
41+
} catch (error) {
42+
const errorMsg = error instanceof Error ? error.message : String(error);
43+
throw new Error(`Failed to observe: ${errorMsg}`);
44+
}
45+
};
46+
47+
return {
48+
action,
49+
captureSnapshot: false,
50+
code: [],
51+
waitForNetwork: false,
52+
};
53+
}
54+
55+
const observeTool: Tool<typeof ObserveInputSchema> = {
56+
capability: "core",
57+
schema: observeSchema,
58+
handle: handleObserve,
59+
};
60+
61+
export default observeTool;

0 commit comments

Comments
 (0)