Hyperbrowser
Official
- src
#!/usr/bin/env node
import { z } from "zod";
import { Ajv } from "ajv";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { downloadImageAsBase64, getClient, logWithTimestamp } from "./utils.js";
const ajv = new Ajv({
coerceTypes: true,
useDefaults: true,
});
// Create server instance
const server = new McpServer({
name: "hyperbrowser",
version: "1.0.7",
});
const sessionOptionsSchema = z
.object({
useProxy: z
.boolean()
.default(false)
.describe("Whether to use a proxy. Recommended true."),
useStealth: z
.boolean()
.default(false)
.describe("Whether to use stealth mode. Recommended false."),
solveCaptchas: z
.boolean()
.default(false)
.describe("Whether to solve captchas. Recommended false."),
acceptCookies: z
.boolean()
.default(false)
.describe(
"Whether to automatically close the accept cookies popup. Recommended false."
),
})
.optional()
.describe(
"Options for the browser session. Avoid setting these if not mentioned explicitly"
);
const apiKeySchema = z
.string()
.optional()
.describe("The API key to use for the scrape");
// Register hyperbrowser tools
server.tool(
"scrape_webpage",
"Scrape a webpage and extract its content in various formats. This tool allows fetching content from a single URL with configurable browser behavior options. Use this for extracting text content, HTML structure, collecting links, or capturing screenshots of webpages.",
{
url: z.string().url().describe("The URL of the webpage to scrape"),
apiKey: apiKeySchema,
sessionOptions: sessionOptionsSchema,
outputFormat: z
.array(z.enum(["markdown", "html", "links", "screenshot"]))
.min(1)
.describe("The format of the output"),
},
async ({
url,
apiKey,
sessionOptions,
outputFormat,
}): Promise<CallToolResult> => {
const currentApiKey =
apiKey ?? process.env.HB_API_KEY ?? process.env.HYPERBROWSER_API_KEY;
if (!currentApiKey) {
return {
content: [
{
type: "text",
text: "No API key provided or found in environment variables",
},
],
isError: true,
};
}
const client = await getClient(currentApiKey);
const result = await client.scrape.startAndWait({
url,
sessionOptions,
scrapeOptions: {
formats: outputFormat,
},
});
if (result.error) {
return {
isError: true,
content: [
{
type: "text",
text: result.error,
},
],
};
}
const response: CallToolResult = {
content: [],
isError: false,
};
if (result.data?.markdown) {
response.content.push({
type: "text",
text: result.data.markdown,
});
}
if (result.data?.html) {
response.content.push({
type: "text",
text: result.data.html,
});
}
if (result.data?.links) {
result.data.links.forEach((link) => {
response.content.push({
type: "resource",
resource: {
uri: link,
text: link,
},
});
});
}
if (result.data?.screenshot) {
const imageData = await downloadImageAsBase64(result.data.screenshot);
if (!imageData) {
response.content.push({
type: "text",
text: "Failed to get screenshot",
});
response.isError = true;
} else {
response.content.push({
type: "image",
data: imageData.data,
mimeType: imageData.mimeType,
});
}
}
return response;
}
);
server.tool(
"extract_structured_data",
"Extract structured data from one or more webpages according to a specified schema. This tool parses webpage content and returns JSON-formatted data based on your prompt instructions. Ideal for extracting product information, article metadata, contact details, or any structured content from websites.",
{
urls: z
.array(z.string().url())
.describe(
"The list of URLs of the webpages to extract structured information from. Can include wildcards (e.g. https://example.com/*)"
),
apiKey: apiKeySchema,
prompt: z.string().describe("The prompt to use for the extraction"),
schema: z
.any({})
.transform((schema) => {
if (!schema) {
return false;
} else {
try {
if (typeof schema === "string") {
try {
const parsedSchema = JSON.parse(schema);
const validate = ajv.compile(parsedSchema);
if (typeof validate === "function") {
return parsedSchema;
} else {
return undefined;
}
} catch (err) {
return undefined;
}
} else {
const validate = ajv.compile(schema);
if (typeof validate === "function") {
return schema;
} else {
return undefined;
}
}
} catch (err) {
return false;
}
}
})
.describe(
"The json schema to use for the extraction. Must provide an object describing a spec compliant json schema, any other types are invalid."
),
sessionOptions: sessionOptionsSchema,
},
async ({
urls,
apiKey,
sessionOptions,
prompt,
schema,
}): Promise<CallToolResult> => {
const currentApiKey =
apiKey ?? process.env.HB_API_KEY ?? process.env.HYPERBROWSER_API_KEY;
if (!currentApiKey) {
return {
content: [
{
type: "text",
text: "No API key provided or found in environment variables",
},
],
isError: true,
};
}
const client = await getClient(currentApiKey);
const params = {
urls,
sessionOptions,
prompt,
schema,
};
const result = await client.extract.startAndWait(params);
if (result.error) {
return {
isError: true,
content: [
{
type: "text",
text: result.error,
},
],
};
}
const response: CallToolResult = {
content: [
{
type: "text",
text: JSON.stringify(result.data, null, 2),
},
],
isError: false,
};
return response;
}
);
server.tool(
"crawl_webpages",
"Crawl a website starting from a URL and explore linked pages. This tool allows systematic collection of content from multiple pages within a domain. Use this for larger data collection tasks, content indexing, or site mapping.",
{
url: z.string().url().describe("The URL of the webpage to crawl."),
apiKey: apiKeySchema,
sessionOptions: sessionOptionsSchema,
outputFormat: z
.array(z.enum(["markdown", "html", "links", "screenshot"]))
.min(1)
.describe("The format of the output"),
followLinks: z
.boolean()
.describe("Whether to follow links on the crawled webpages"),
maxPages: z
.number()
.int()
.positive()
.finite()
.safe()
.min(1)
.max(1000)
.default(10),
ignoreSitemap: z.boolean().default(false),
},
async ({
url,
apiKey,
sessionOptions,
outputFormat,
ignoreSitemap,
followLinks,
maxPages,
}): Promise<CallToolResult> => {
const currentApiKey =
apiKey ?? process.env.HB_API_KEY ?? process.env.HYPERBROWSER_API_KEY;
if (!currentApiKey) {
return {
content: [
{
type: "text",
text: "No API key provided or found in environment variables",
},
],
isError: true,
};
}
const client = await getClient(currentApiKey);
const result = await client.crawl.startAndWait({
url,
sessionOptions,
scrapeOptions: {
formats: outputFormat,
},
maxPages,
ignoreSitemap,
followLinks,
});
if (result.error) {
return {
isError: true,
content: [
{
type: "text",
text: result.error,
},
],
};
}
const response: CallToolResult = {
content: [],
isError: false,
};
result.data?.forEach((page) => {
if (page?.markdown) {
response.content.push({
type: "text",
text: page.markdown,
});
}
if (page?.html) {
response.content.push({
type: "text",
text: page.html,
});
}
if (page?.links) {
page.links.forEach((link) => {
response.content.push({
type: "resource",
resource: {
uri: link,
text: link,
},
});
});
}
if (page?.screenshot) {
response.content.push({
type: "image",
data: page.screenshot,
mimeType: "image/webp",
});
}
});
return response;
}
);
server.tool(
"browser_use",
"Perform a certain task inside a browser session. Will perform the entirety of the task inside the browser, and return the results.",
{
task: z.string().describe("The task to perform inside the browser"),
apiKey: apiKeySchema,
sessionOptions: sessionOptionsSchema,
returnStepInfo: z
.boolean()
.default(false)
.describe(
"Whether to return step-by-step information about the task.Should be false by default. May contain excessive information."
),
maxSteps: z
.number()
.int()
.positive()
.finite()
.safe()
.min(1)
.max(1000)
.default(10),
},
async ({
task,
apiKey,
sessionOptions,
returnStepInfo,
maxSteps,
}): Promise<CallToolResult> => {
const currentApiKey =
apiKey ?? process.env.HB_API_KEY ?? process.env.HYPERBROWSER_API_KEY;
if (!currentApiKey) {
return {
content: [
{
type: "text",
text: "No API key provided or found in environment variables",
},
],
isError: true,
};
}
const client = await getClient(currentApiKey);
const result = await client.beta.agents.browserUse.startAndWait({
task,
sessionOptions,
maxSteps,
});
if (result.error) {
return {
isError: true,
content: [
{
type: "text",
text: result.error,
},
],
};
}
const response: CallToolResult = {
content: [],
isError: false,
};
if (result.data) {
let taskData = result.data;
if (!returnStepInfo) {
taskData.steps = [];
}
response.content.push({
type: "text",
text: JSON.stringify(taskData),
});
} else {
response.content.push({
type: "text",
text: "Task result data is empty/missing",
isError: true,
});
}
return response;
}
);
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
logWithTimestamp({ data: "hyperbrowser MCP Server running on stdio" });
}
main().catch((error) => {
logWithTimestamp({
level: "error",
data: ["Fatal error in main():", error],
});
process.exit(1);
});