mcp-server-browserbase
Official
by browserbase
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
CallToolResult,
TextContent,
ImageContent,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import puppeteer, { Browser, Page } from "puppeteer-core";
import { Browserbase } from "@browserbasehq/sdk";
// Environment variables configuration
const requiredEnvVars = {
BROWSERBASE_API_KEY: process.env.BROWSERBASE_API_KEY,
BROWSERBASE_PROJECT_ID: process.env.BROWSERBASE_PROJECT_ID,
};
// Validate required environment variables
Object.entries(requiredEnvVars).forEach(([name, value]) => {
if (!value) throw new Error(`${name} environment variable is required`);
});
// 2. Global State
const browsers = new Map<string, { browser: Browser; page: Page }>();
const consoleLogs: string[] = [];
const screenshots = new Map<string, string>();
// 3. Helper Functions
async function createNewBrowserSession(sessionId: string) {
const bb = new Browserbase({
apiKey: process.env.BROWSERBASE_API_KEY!,
});
const session = await bb.sessions.create({
projectId: process.env.BROWSERBASE_PROJECT_ID!,
});
const browser = await puppeteer.connect({
browserWSEndpoint: session.connectUrl,
});
const page = (await browser.pages())[0];
browsers.set(sessionId, { browser, page });
// Set up console logging for this session
page.on("console", (msg) => {
const logEntry = `[Session ${sessionId}][${msg.type()}] ${msg.text()}`;
consoleLogs.push(logEntry);
server.notification({
method: "notifications/cloud/message",
params: { message: logEntry, type: "console_log" },
});
});
return { browser, page };
}
// 4. Tool Definitions
const TOOLS: Tool[] = [
{
name: "browserbase_create_session",
description: "Create a new cloud browser session using Browserbase",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "browserbase_close_session",
description: "Close a browser session on Browserbase",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string" },
},
required: ["sessionId"],
},
},
{
name: "browserbase_navigate",
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string" },
},
required: ["url"],
},
},
{
name: "browserbase_screenshot",
description: "Take a screenshot of the current page or a specific element",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name for the screenshot" },
selector: {
type: "string",
description: "CSS selector for element to screenshot",
},
width: {
type: "number",
description: "Width in pixels (default: 800)",
},
height: {
type: "number",
description: "Height in pixels (default: 600)",
},
},
required: ["name"],
},
},
{
name: "browserbase_click",
description: "Click an element on the page",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for element to click",
},
},
required: ["selector"],
},
},
{
name: "browserbase_fill",
description: "Fill out an input field",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for input field",
},
value: { type: "string", description: "Value to fill" },
},
required: ["selector", "value"],
},
},
{
name: "browserbase_evaluate",
description: "Execute JavaScript in the browser console",
inputSchema: {
type: "object",
properties: {
script: { type: "string", description: "JavaScript code to execute" },
},
required: ["script"],
},
},
{
name: "browserbase_get_content",
description: "Extract all content from the current page",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description:
"Optional CSS selector to get content from specific elements (default: returns whole page)",
},
},
required: [],
},
}
];
// 5. Tool Handler Implementation
async function handleToolCall(
name: string,
args: any
): Promise<CallToolResult> {
// Only auto-create sessions for tools OTHER than create_session
const defaultSession = !["browserbase_create_session"].includes(name)
? browsers.get(args.sessionId) ||
(await createNewBrowserSession(args.sessionId))
: null;
switch (name) {
case "browserbase_close_session":
await defaultSession!.browser.close();
browsers.delete(args.sessionId);
return {
content: [{ type: "text", text: "Closed session" }],
};
case "browserbase_create_session":
try {
// Check if session already exists
if (browsers.has(args.sessionId)) {
return {
content: [
{
type: "text",
text: "Session already exists",
},
],
isError: false,
};
}
await createNewBrowserSession(args.sessionId);
return {
content: [
{
type: "text",
text: "Created new browser session",
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to create browser session: ${
(error as Error).message
}`,
},
],
isError: true,
};
}
case "browserbase_navigate":
await defaultSession!.page.goto(args.url);
return {
content: [
{
type: "text",
text: `Navigated to ${args.url}`,
},
],
isError: false,
};
case "browserbase_screenshot": {
const width = args.width ?? 800;
const height = args.height ?? 600;
await defaultSession!.page.setViewport({ width, height });
const screenshot = await (args.selector
? (
await defaultSession!.page.$(args.selector)
)?.screenshot({ encoding: "base64" })
: defaultSession!.page.screenshot({
encoding: "base64",
fullPage: false,
}));
if (!screenshot) {
return {
content: [
{
type: "text",
text: args.selector
? `Element not found: ${args.selector}`
: "Screenshot failed",
},
],
isError: true,
};
}
screenshots.set(args.name, screenshot as string);
server.notification({
method: "notifications/resources/list_changed",
});
return {
content: [
{
type: "text",
text: `Screenshot '${args.name}' taken at ${width}x${height}`,
} as TextContent,
{
type: "image",
data: screenshot,
mimeType: "image/png",
} as ImageContent,
],
isError: false,
};
}
case "browserbase_click":
try {
await defaultSession!.page.click(args.selector);
return {
content: [
{
type: "text",
text: `Clicked: ${args.selector}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to click ${args.selector}: ${
(error as Error).message
}`,
},
],
isError: true,
};
}
case "browserbase_fill":
try {
await defaultSession!.page.waitForSelector(args.selector);
await defaultSession!.page.type(args.selector, args.value);
return {
content: [
{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to fill ${args.selector}: ${
(error as Error).message
}`,
},
],
isError: true,
};
}
case "browserbase_evaluate":
try {
const result = await defaultSession!.page.evaluate((script) => {
const logs: string[] = [];
const originalConsole = { ...console };
["log", "info", "warn", "error"].forEach((method) => {
(console as any)[method] = (...args: any[]) => {
logs.push(`[${method}] ${args.join(" ")}`);
(originalConsole as any)[method](...args);
};
});
try {
const result = eval(script);
Object.assign(console, originalConsole);
return { result, logs };
} catch (error) {
Object.assign(console, originalConsole);
throw error;
}
}, args.script);
return {
content: [
{
type: "text",
text: `Execution result:\n${JSON.stringify(
result.result,
null,
2
)}\n\nConsole output:\n${result.logs.join("\n")}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Script execution failed: ${(error as Error).message}`,
},
],
isError: true,
};
}
case "browserbase_get_json":
try {
const result = await defaultSession!.page.evaluate((selector) => {
// Helper function to find JSON in text
function extractJSON(text: string) {
const jsonObjects = [];
let braceCount = 0;
let start = -1;
for (let i = 0; i < text.length; i++) {
if (text[i] === "{") {
if (braceCount === 0) start = i;
braceCount++;
} else if (text[i] === "}") {
braceCount--;
if (braceCount === 0 && start !== -1) {
try {
const jsonStr = text.slice(start, i + 1);
const parsed = JSON.parse(jsonStr);
jsonObjects.push(parsed);
} catch (e) {
// Invalid JSON, continue searching
}
}
}
}
return jsonObjects;
}
// Get all text content based on selector or full page
const elements = selector
? Array.from(document.querySelectorAll(selector))
: [document.body];
const results = {
// Look for JSON in text content
textContent: elements.flatMap((el) =>
extractJSON(el.textContent || "")
),
// Look for JSON in script tags
scriptTags: Array.from(
document.getElementsByTagName("script")
).flatMap((script) => {
try {
if (script.type === "application/json") {
return [JSON.parse(script.textContent || "")];
}
return extractJSON(script.textContent || "");
} catch (e) {
return [];
}
}),
// Look for JSON in meta tags
metaTags: Array.from(document.getElementsByTagName("meta")).flatMap(
(meta) => {
try {
const content = meta.getAttribute("content") || "";
return extractJSON(content);
} catch (e) {
return [];
}
}
),
// Look for JSON-LD
jsonLd: Array.from(
document.querySelectorAll('script[type="application/ld+json"]')
).flatMap((script) => {
try {
return [JSON.parse(script.textContent || "")];
} catch (e) {
return [];
}
}),
};
return results;
}, args.selector);
return {
content: [
{
type: "text",
text: `Found JSON content:\n${JSON.stringify(result, null, 2)}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to extract JSON: ${(error as Error).message}`,
},
],
isError: true,
};
}
case "browserbase_get_content":
try {
let content;
if (args.selector) {
// If selector is provided, get content from specific elements
content = await defaultSession!.page.evaluate((selector) => {
const elements = document.querySelectorAll(selector);
return Array.from(elements).map((el) => el.textContent || "");
}, args.selector);
} else {
// If no selector is provided, get content from the whole page
content = await defaultSession!.page.evaluate(() => {
return Array.from(document.querySelectorAll("*")).map(
(el) => el.textContent || ""
);
});
}
return {
content: [
{
type: "text",
text: `Extracted content:\n${JSON.stringify(content, null, 2)}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to extract content: ${(error as Error).message}`,
},
],
isError: true,
};
}
default:
return {
content: [
{
type: "text",
text: `Unknown tool: ${name}`,
},
],
isError: true,
};
}
}
// 6. Server Setup and Configuration
const server = new Server(
{
name: "example-servers/browserbase",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// 7. Request Handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
},
...Array.from(screenshots.keys()).map((name) => ({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
})),
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri.toString();
if (uri === "console://logs") {
return {
contents: [
{
uri,
mimeType: "text/plain",
text: consoleLogs.join("\n"),
},
],
};
}
if (uri.startsWith("screenshot://")) {
const name = uri.split("://")[1];
const screenshot = screenshots.get(name);
if (screenshot) {
return {
contents: [
{
uri,
mimeType: "image/png",
blob: screenshot,
},
],
};
}
}
throw new Error(`Resource not found: ${uri}`);
});
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments ?? {})
);
// 8. Server Initialization
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);