Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @browserbasehq/mcp-server-browserbase

## 2.1.2

### Patch Changes

- fixing screenshot map behavior

## 2.1.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@browserbasehq/mcp-server-browserbase",
"version": "2.1.1",
"version": "2.1.2",
"description": "MCP server for AI web browser automation using Browserbase and Stagehand",
"mcpName": "io.github.browserbase/mcp-server-browserbase",
"license": "Apache-2.0",
Expand Down
33 changes: 33 additions & 0 deletions src/mcp/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,39 @@ export const RESOURCE_TEMPLATES = [];
// Store screenshots in a map
export const screenshots = new Map<string, string>();

// Track screenshots by session so we can purge them on session end
// key: sessionId (internal/current session id), value: set of screenshot names
const sessionIdToScreenshotNames = new Map<string, Set<string>>();

export function registerScreenshot(
sessionId: string,
name: string,
base64: string,
) {
screenshots.set(name, base64);
let set = sessionIdToScreenshotNames.get(sessionId);
if (!set) {
set = new Set();
sessionIdToScreenshotNames.set(sessionId, set);
}
set.add(name);
}

export function clearScreenshotsForSession(sessionId: string) {
const set = sessionIdToScreenshotNames.get(sessionId);
if (set) {
for (const name of set) {
screenshots.delete(name);
}
sessionIdToScreenshotNames.delete(sessionId);
}
}

export function clearAllScreenshots() {
screenshots.clear();
sessionIdToScreenshotNames.clear();
}

/**
* Handle listing resources request
* @returns A list of available resources including screenshots
Expand Down
41 changes: 41 additions & 0 deletions src/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Page, BrowserContext } from "@browserbasehq/stagehand";
import type { Config } from "../config.d.ts";
import type { Cookie } from "playwright-core";
import { createStagehandInstance } from "./stagehandStore.js";
import { clearScreenshotsForSession } from "./mcp/resources.js";
import type { BrowserSession } from "./types/types.js";

// Global state for managing browser sessions
Expand Down Expand Up @@ -131,6 +132,21 @@ export async function createNewBrowserSession(
);
setActiveSessionId(defaultSessionId);
}

// Purge any screenshots associated with both internal and Browserbase IDs
try {
clearScreenshotsForSession(newSessionId);
const bbId = browserbaseSessionId;
if (bbId) {
clearScreenshotsForSession(bbId);
}
} catch (err) {
process.stderr.write(
`[SessionManager] WARN - Failed to clear screenshots on disconnect for ${newSessionId}: ${
err instanceof Error ? err.message : String(err)
}\n`,
);
Comment on lines +135 to +148
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Screenshot cleanup is called in a disconnect handler that runs during session creation, but the session might not be fully established yet. This could lead to premature cleanup of screenshots that are still being used.

Suggested change
// Purge any screenshots associated with both internal and Browserbase IDs
try {
clearScreenshotsForSession(newSessionId);
const bbId = browserbaseSessionId;
if (bbId) {
clearScreenshotsForSession(bbId);
}
} catch (err) {
process.stderr.write(
`[SessionManager] WARN - Failed to clear screenshots on disconnect for ${newSessionId}: ${
err instanceof Error ? err.message : String(err)
}\n`,
);
// Only clear screenshots if the session was actually established
if (session?.stagehand) {
try {
clearScreenshotsForSession(newSessionId);
const bbId = browserbaseSessionId;
if (bbId) {
clearScreenshotsForSession(bbId);
}
} catch (err) {
process.stderr.write(
`[SessionManager] WARN - Failed to clear screenshots on disconnect for ${newSessionId}: ${
err instanceof Error ? err.message : String(err)
}\n`,
);
}
}

}
});

// Add cookies to the context if they are provided in the config
Expand Down Expand Up @@ -192,6 +208,20 @@ async function closeBrowserGracefully(
process.stderr.write(
`[SessionManager] Successfully closed Stagehand and browser for session: ${sessionIdToLog}\n`,
);
// After close, purge any screenshots associated with both internal and Browserbase IDs
try {
clearScreenshotsForSession(sessionIdToLog);
const bbId = session?.stagehand?.browserbaseSessionID;
if (bbId) {
clearScreenshotsForSession(bbId);
}
} catch (err) {
process.stderr.write(
`[SessionManager] WARN - Failed to clear screenshots after close for ${sessionIdToLog}: ${
err instanceof Error ? err.message : String(err)
}\n`,
);
}
} catch (closeError) {
process.stderr.write(
`[SessionManager] WARN - Error closing Stagehand for session ${sessionIdToLog}: ${
Expand Down Expand Up @@ -335,6 +365,17 @@ export async function cleanupSession(sessionId: string): Promise<void> {
// Remove from browsers map
browsers.delete(sessionId);

// Always purge screenshots for this (internal) session id
try {
clearScreenshotsForSession(sessionId);
} catch (err) {
process.stderr.write(
`[SessionManager] WARN - Failed to clear screenshots during cleanup for ${sessionId}: ${
err instanceof Error ? err.message : String(err)
}\n`,
);
}

// Clear default session reference if this was the default
if (sessionId === defaultSessionId && defaultBrowserSession) {
defaultBrowserSession = null;
Expand Down
25 changes: 25 additions & 0 deletions src/stagehandStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from "crypto";
import { Stagehand, Page } from "@browserbasehq/stagehand";
import { StagehandSession, CreateSessionParams } from "./types/types.js";
import type { Config } from "../config.d.ts";
import { clearScreenshotsForSession } from "./mcp/resources.js";

// Store for all active sessions
const store = new Map<string, StagehandSession>();
Expand Down Expand Up @@ -110,6 +111,18 @@ export const create = async (
const disconnectHandler = () => {
process.stderr.write(`[StagehandStore] Session disconnected: ${id}\n`);
store.delete(id);
// Purge by internal store ID and Browserbase session ID
try {
clearScreenshotsForSession(id);
const bbId = session.metadata?.bbSessionId;
if (bbId) {
clearScreenshotsForSession(bbId);
}
} catch {
process.stderr.write(
`[StagehandStore] Error clearing screenshots for session ${id}\n`,
);
}
Comment on lines +120 to +125
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Silent error handling might hide important failures during screenshot cleanup.

Suggested change
}
} catch {
process.stderr.write(
`[StagehandStore] Error clearing screenshots for session ${id}\n`,
);
}
} catch (err) {
process.stderr.write(
`[StagehandStore] Error clearing screenshots for session ${id}: ${
err instanceof Error ? err.message : String(err)
}\n`,
);
}

};

browser.on("disconnected", disconnectHandler);
Expand Down Expand Up @@ -158,6 +171,18 @@ export const remove = async (id: string): Promise<void> => {

await session.stagehand.close();
process.stderr.write(`[StagehandStore] Session closed: ${id}\n`);
// Purge by internal store ID and Browserbase session ID
try {
clearScreenshotsForSession(id);
const bbId = session.metadata?.bbSessionId;
if (bbId) {
clearScreenshotsForSession(bbId);
}
} catch {
process.stderr.write(
`[StagehandStore] Error clearing screenshots for session ${id}\n`,
);
}
} catch (error) {
process.stderr.write(
Comment on lines +182 to 187
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Silent error handling might hide important failures during screenshot cleanup.

Suggested change
process.stderr.write(
`[StagehandStore] Error clearing screenshots for session ${id}\n`,
);
}
} catch (error) {
process.stderr.write(
} catch (err) {
process.stderr.write(
`[StagehandStore] Error clearing screenshots for session ${id}: ${
err instanceof Error ? err.message : String(err)
}\n`,
);
}

`[StagehandStore] Error closing session ${id}: ${
Expand Down
6 changes: 4 additions & 2 deletions src/tools/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { z } from "zod";
import type { Tool, ToolSchema, ToolResult } from "./tool.js";
import type { Context } from "../context.js";
import type { ToolActionResult } from "../types/types.js";
import { screenshots } from "../mcp/resources.js";
import { registerScreenshot } from "../mcp/resources.js";

const ScreenshotInputSchema = z.object({
name: z.string().optional().describe("The name of the screenshot"),
Expand Down Expand Up @@ -40,7 +40,9 @@ async function handleScreenshot(
.replace(/:/g, "-")}`
: `screenshot-${new Date().toISOString().replace(/:/g, "-")}` +
context.config.browserbaseProjectId;
screenshots.set(name, screenshotBase64);
// Associate with current session id and store in memory
const sessionId = context.currentSessionId;
registerScreenshot(sessionId, name, screenshotBase64);

// Notify the client that the resources changed
const serverInstance = context.getServer();
Expand Down