diff --git a/stagehand/package.json b/stagehand/package.json
index 9b8f327..d9f4165 100644
--- a/stagehand/package.json
+++ b/stagehand/package.json
@@ -15,6 +15,7 @@
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
+ "dev": "npm run build && STAGEHAND_HTTP_PORT=8081 node dist/index.js",
"prepare": "npm run build",
"watch": "tsc --watch"
},
diff --git a/stagehand/src/httpStaticServer.ts b/stagehand/src/httpStaticServer.ts
new file mode 100644
index 0000000..7568311
--- /dev/null
+++ b/stagehand/src/httpStaticServer.ts
@@ -0,0 +1,94 @@
+import http from "http";
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Function to clean the tmp directory
+function cleanTmpDirectory(directory: string) {
+ if (!fs.existsSync(directory)) {
+ fs.mkdirSync(directory, { recursive: true });
+ return;
+ }
+
+ try {
+ const files = fs.readdirSync(directory);
+ for (const file of files) {
+ if (file.startsWith('.')) continue; // Skip hidden files
+
+ const filePath = path.join(directory, file);
+ fs.unlinkSync(filePath);
+ }
+ console.log(`Cleaned tmp directory: ${directory}`);
+ } catch (err) {
+ console.error(`Error cleaning tmp directory: ${err}`);
+ }
+}
+
+export function startStaticHttpServer() {
+ const TMP_DIR = path.resolve(__dirname, "../tmp");
+ const HTTP_PORT = process.env.STAGEHAND_HTTP_PORT ? parseInt(process.env.STAGEHAND_HTTP_PORT, 10) : 8080;
+
+ // Clean the tmp directory on startup
+ cleanTmpDirectory(TMP_DIR);
+
+ const server = http.createServer((req, res) => {
+ if (!req.url) {
+ res.writeHead(400);
+ res.end("Bad Request");
+ return;
+ }
+ // Only allow /tmp/ URLs
+ if (!req.url.startsWith("/tmp/")) {
+ res.writeHead(404);
+ res.end("Not Found");
+ return;
+ }
+ // Directory listing for /tmp/ or /tmp
+ if (req.url === "/tmp/" || req.url === "/tmp") {
+ fs.readdir(TMP_DIR, (err, files) => {
+ if (err) {
+ res.writeHead(500);
+ res.end("Failed to read directory");
+ return;
+ }
+ const links = files
+ .filter(f => !f.startsWith("."))
+ .map(f => `
${f}`) // encode for safety
+ .join("\n");
+ const html = `tmp Directory ListingFiles in /tmp/
`;
+ res.writeHead(200, { "Content-Type": "text/html" });
+ res.end(html);
+ });
+ return;
+ }
+ // Serve individual files
+ const filePath = path.join(TMP_DIR, req.url.replace("/tmp/", ""));
+ if (!filePath.startsWith(TMP_DIR)) {
+ res.writeHead(403);
+ res.end("Forbidden");
+ return;
+ }
+ fs.readFile(filePath, (err, data) => {
+ if (err) {
+ res.writeHead(404);
+ res.end("Not Found");
+ return;
+ }
+ res.writeHead(200, { "Content-Type": "text/html" });
+ res.end(data);
+ });
+ });
+ let actualPort = HTTP_PORT;
+ server.listen(HTTP_PORT, () => {
+ const address = server.address();
+ if (address && typeof address === 'object') {
+ actualPort = address.port;
+ }
+ // eslint-disable-next-line no-console
+ console.log(`Static file server running at http://localhost:${actualPort}/tmp/`);
+ });
+ return { server, port: actualPort };
+}
\ No newline at end of file
diff --git a/stagehand/src/index.ts b/stagehand/src/index.ts
index 74a35fe..52bab08 100644
--- a/stagehand/src/index.ts
+++ b/stagehand/src/index.ts
@@ -2,7 +2,13 @@
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./server.js";
-import { ensureLogDirectory, registerExitHandlers, scheduleLogRotation, setupLogRotation } from "./logging.js";
+import {
+ ensureLogDirectory,
+ registerExitHandlers,
+ scheduleLogRotation,
+ setupLogRotation,
+} from "./logging.js";
+import { startStaticHttpServer } from "./httpStaticServer.js";
// Run setup for logging
ensureLogDirectory();
@@ -10,6 +16,9 @@ setupLogRotation();
scheduleLogRotation();
registerExitHandlers();
+// Start the static HTTP server for /tmp and capture the port
+const { port: staticHttpPort } = startStaticHttpServer();
+
// Run the server
async function runServer() {
const server = createServer();
@@ -17,7 +26,7 @@ async function runServer() {
await server.connect(transport);
server.sendLoggingMessage({
level: "info",
- data: "Stagehand MCP server is ready to accept requests",
+ data: `Stagehand MCP server is ready to accept requests. Static HTTP server running on port ${staticHttpPort}`,
});
}
diff --git a/stagehand/src/tools.ts b/stagehand/src/tools.ts
index e04a39e..c991904 100644
--- a/stagehand/src/tools.ts
+++ b/stagehand/src/tools.ts
@@ -2,6 +2,11 @@ import { Stagehand } from "@browserbasehq/stagehand";
import { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
import { getServerInstance, operationLogs } from "./logging.js";
import { screenshots } from "./resources.js";
+import { fileURLToPath } from "url";
+import path from "path";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
// Define the Stagehand tools
export const TOOLS: Tool[] = [
@@ -69,6 +74,21 @@ export const TOOLS: Tool[] = [
required: ["instruction"],
},
},
+ {
+ name: "stagehand_get_html",
+ description:
+ "Captures the raw HTML of the current webpage. Use this tool when you need to analyze the page structure or extract specific HTML elements. This tool returns a URL that you need to download to access the HTML content.",
+ inputSchema: {
+ type: "object",
+ properties: {
+ selector: {
+ type: "string",
+ description:
+ "Optional selector to get HTML for a specific element. Both CSS and XPath selectors are supported. If omitted, returns the entire page HTML.",
+ },
+ },
+ },
+ },
{
name: "screenshot",
description:
@@ -233,6 +253,146 @@ export async function handleToolCall(
};
}
+ case "stagehand_get_html":
+ try {
+ const html = await stagehand.page.evaluate((selector) => {
+ if (selector) {
+ try {
+ // Check if the selector is an XPath selector
+ if (
+ selector.startsWith("/") ||
+ selector.startsWith("./") ||
+ selector.startsWith("//")
+ ) {
+ // Handle XPath selector
+ const result = document.evaluate(
+ selector,
+ document,
+ null,
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
+ null
+ );
+ const element = result.singleNodeValue;
+ if (!element || !(element instanceof Element)) {
+ return `
+
+XPath Element Not Found
+
+XPath Element Not Found
+The XPath selector did not match any elements on the page.
+Details:
+
+ - XPath Selector: ${selector}
+ - Document Title: ${document.title}
+ - Document URL: ${document.location.href}
+ - Page Content Length: ${document.documentElement.outerHTML.length} characters
+
+Suggestions:
+
+ - Check if the XPath selector is correct
+ - Verify that the element exists on the page
+ - Try using browser developer tools to test the XPath selector
+ - Consider using a CSS selector instead if possible
+
+
+`;
+ }
+ return element.outerHTML;
+ } else {
+ // Handle CSS selector
+ const element = document.querySelector(selector);
+ if (!element || !(element instanceof Element)) {
+ return `
+
+CSS Element Not Found
+
+CSS Element Not Found
+The CSS selector did not match any elements on the page.
+Details:
+
+ - CSS Selector: ${selector}
+ - Document Title: ${document.title}
+ - Document URL: ${document.location.href}
+ - Page Content Length: ${
+ document.documentElement.outerHTML.length
+ } characters
+ - Similar Elements: ${
+ Array.from(document.querySelectorAll("*"))
+ .filter(
+ (el) =>
+ el.tagName.toLowerCase() ===
+ selector.split(/[.#\[\s>+~]/)[0].toLowerCase()
+ )
+ .slice(0, 5)
+ .map(
+ (el) =>
+ `<${el.tagName.toLowerCase()}${el.id ? ` id="${el.id}"` : ""}${
+ el.className ? ` class="${el.className}"` : ""
+ }>`
+ )
+ .join(", ") || "None found"
+ }
+
+Suggestions:
+
+ - Check if the CSS selector syntax is correct
+ - Verify that the element exists on the page
+ - Try using browser developer tools to test the CSS selector
+ - Consider using a simpler selector (e.g., by ID or a unique class)
+ - Check if the element is dynamically added to the page
+
+
+`;
+ }
+ return element.outerHTML;
+ }
+ } catch (err: unknown) {
+ return `Selector error: ${
+ err instanceof Error ? err.message : String(err)
+ }. For XPath, use '//' or '/' prefix. For CSS, use standard selectors.`;
+ }
+ }
+ return document.documentElement.outerHTML;
+ }, args.selector || null);
+
+ // Save HTML to a file in the tmp directory
+ const fs = await import("fs/promises");
+ const { randomBytes } = await import("crypto");
+ const TMP_DIR = path.resolve(__dirname, "../tmp");
+ await fs.mkdir(TMP_DIR, { recursive: true });
+ const unique = `${Date.now()}-${randomBytes(6).toString("hex")}`;
+ const filename = `stagehand-html-${unique}.html`;
+ const filePath = path.join(TMP_DIR, filename);
+ await fs.writeFile(filePath, html, "utf8");
+ const port = process.env.STAGEHAND_HTTP_PORT || 8080;
+ const url = `http://localhost:${port}/tmp/${filename}`;
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: `HTML saved to: ${url}`,
+ },
+ ],
+ isError: false,
+ };
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Failed to get HTML: ${errorMsg}`,
+ },
+ {
+ type: "text",
+ text: `Operation logs:\n${operationLogs.join("\n")}`,
+ },
+ ],
+ isError: true,
+ };
+ }
+
case "screenshot":
try {
const screenshotBuffer = await stagehand.page.screenshot({