Skip to main content
Glama

mcp-server-browserbase

Official
by browserbase
Apache 2.0
2,054
2,725
sessionManager.ts19.4 kB
import { BrowserContext, Stagehand } from "@browserbasehq/stagehand"; import type { Config } from "../config.d.ts"; import type { Cookie } from "playwright-core"; import { clearScreenshotsForSession } from "./mcp/resources.js"; import type { BrowserSession, CreateSessionParams } from "./types/types.js"; import { randomUUID } from "crypto"; /** * Create a configured Stagehand instance * This is used internally by SessionManager to initialize browser sessions */ export const createStagehandInstance = async ( config: Config, params: CreateSessionParams = {}, sessionId: string, ): Promise<Stagehand> => { const apiKey = params.apiKey || config.browserbaseApiKey; const projectId = params.projectId || config.browserbaseProjectId; if (!apiKey || !projectId) { throw new Error("Browserbase API Key and Project ID are required"); } const stagehand = new Stagehand({ env: "BROWSERBASE", apiKey, projectId, modelName: params.modelName || config.modelName || "gemini-2.0-flash", modelClientOptions: { apiKey: config.modelApiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY, }, ...(params.browserbaseSessionID && { browserbaseSessionID: params.browserbaseSessionID, }), experimental: config.experimental ?? false, browserbaseSessionCreateParams: { projectId, proxies: config.proxies, keepAlive: config.keepAlive ?? false, browserSettings: { viewport: { width: config.viewPort?.browserWidth ?? 1024, height: config.viewPort?.browserHeight ?? 768, }, context: config.context?.contextId ? { id: config.context?.contextId, persist: config.context?.persist ?? true, } : undefined, advancedStealth: config.advancedStealth ?? undefined, }, userMetadata: { mcp: "true", }, }, logger: (logLine) => { console.error(`Stagehand[${sessionId}]: ${logLine.message}`); }, }); await stagehand.init(); return stagehand; }; /** * SessionManager manages browser sessions and tracks active/default sessions. * * Session ID Strategy: * - Default session: Uses generated ID with timestamp and UUID for uniqueness * - User sessions: Uses raw sessionId provided by user (no suffix added) * - All sessions stored in this.browsers Map with their internal ID as key * * Note: Context.currentSessionId is a getter that delegates to this.getActiveSessionId() * to ensure session tracking stays synchronized. */ export class SessionManager { private browsers: Map<string, BrowserSession>; private defaultBrowserSession: BrowserSession | null; private readonly defaultSessionId: string; private activeSessionId: string; // Mutex to prevent race condition when multiple calls try to create default session simultaneously private defaultSessionCreationPromise: Promise<BrowserSession> | null = null; // Track sessions currently being cleaned up to prevent concurrent cleanup private cleaningUpSessions: Set<string> = new Set(); constructor(contextId?: string) { this.browsers = new Map(); this.defaultBrowserSession = null; const uniqueId = randomUUID(); this.defaultSessionId = `browserbase_session_${contextId || "default"}_${Date.now()}_${uniqueId}`; this.activeSessionId = this.defaultSessionId; } getDefaultSessionId(): string { return this.defaultSessionId; } /** * Sets the active session ID. * @param id The ID of the session to set as active. */ setActiveSessionId(id: string): void { if (this.browsers.has(id)) { this.activeSessionId = id; } else if (id === this.defaultSessionId) { // Allow setting to default ID even if session doesn't exist yet // (it will be created on first use via ensureDefaultSessionInternal) this.activeSessionId = id; } else { process.stderr.write( `[SessionManager] WARN - Set active session failed for non-existent ID: ${id}\n`, ); } } /** * Gets the active session ID. * @returns The active session ID. */ getActiveSessionId(): string { return this.activeSessionId; } /** * Adds cookies to a browser context * @param context Playwright browser context * @param cookies Array of cookies to add */ async addCookiesToContext( context: BrowserContext, cookies: Cookie[], ): Promise<void> { if (!cookies || cookies.length === 0) { return; } try { process.stderr.write( `[SessionManager] Adding ${cookies.length} cookies to browser context\n`, ); // Injecting cookies into the Browser Context await context.addCookies(cookies); process.stderr.write( `[SessionManager] Successfully added cookies to browser context\n`, ); } catch (error) { process.stderr.write( `[SessionManager] Error adding cookies to browser context: ${ error instanceof Error ? error.message : String(error) }\n`, ); } } /** * Creates a new Browserbase session using Stagehand. * @param newSessionId - Internal session ID for tracking in SessionManager * @param config - Configuration object * @param resumeSessionId - Optional Browserbase session ID to resume/reuse */ async createNewBrowserSession( newSessionId: string, config: Config, resumeSessionId?: string, ): Promise<BrowserSession> { if (!config.browserbaseApiKey) { throw new Error("Browserbase API Key is missing in the configuration."); } if (!config.browserbaseProjectId) { throw new Error( "Browserbase Project ID is missing in the configuration.", ); } try { process.stderr.write( `[SessionManager] ${resumeSessionId ? "Resuming" : "Creating"} Stagehand session ${newSessionId}...\n`, ); // Create and initialize Stagehand instance using shared function const stagehand = await createStagehandInstance( config, { ...(resumeSessionId && { browserbaseSessionID: resumeSessionId }), }, newSessionId, ); // Get the page and browser from Stagehand const page = stagehand.page; const browser = page.context().browser(); if (!browser) { throw new Error("Failed to get browser from Stagehand page context"); } const browserbaseSessionId = stagehand.browserbaseSessionID; if (!browserbaseSessionId) { throw new Error( "Browserbase session ID is required but was not returned by Stagehand", ); } process.stderr.write( `[SessionManager] Stagehand initialized with Browserbase session: ${browserbaseSessionId}\n`, ); process.stderr.write( `[SessionManager] Browserbase Live Debugger URL: https://www.browserbase.com/sessions/${browserbaseSessionId}\n`, ); // Set up disconnect handler browser.on("disconnected", () => { process.stderr.write( `[SessionManager] Disconnected: ${newSessionId}\n`, ); this.browsers.delete(newSessionId); if ( this.defaultBrowserSession && this.defaultBrowserSession.browser === browser ) { process.stderr.write( `[SessionManager] Disconnected (default): ${newSessionId}\n`, ); this.defaultBrowserSession = null; // Reset active session to default ID since default session needs recreation this.setActiveSessionId(this.defaultSessionId); } if ( this.activeSessionId === newSessionId && newSessionId !== this.defaultSessionId ) { process.stderr.write( `[SessionManager] WARN - Active session disconnected, resetting to default: ${newSessionId}\n`, ); this.setActiveSessionId(this.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`, ); } }); // Add cookies to the context if they are provided in the config if ( config.cookies && Array.isArray(config.cookies) && config.cookies.length > 0 ) { await this.addCookiesToContext( page.context() as BrowserContext, config.cookies, ); } const sessionObj: BrowserSession = { browser, page, sessionId: browserbaseSessionId, stagehand, }; this.browsers.set(newSessionId, sessionObj); if (newSessionId === this.defaultSessionId) { this.defaultBrowserSession = sessionObj; } this.setActiveSessionId(newSessionId); process.stderr.write( `[SessionManager] Session created and active: ${newSessionId}\n`, ); return sessionObj; } catch (creationError) { const errorMessage = creationError instanceof Error ? creationError.message : String(creationError); process.stderr.write( `[SessionManager] Creating session ${newSessionId} failed: ${errorMessage}\n`, ); throw new Error( `Failed to create/connect session ${newSessionId}: ${errorMessage}`, ); } } private async closeBrowserGracefully( session: BrowserSession | undefined | null, sessionIdToLog: string, ): Promise<void> { // Check if this session is already being cleaned up if (this.cleaningUpSessions.has(sessionIdToLog)) { process.stderr.write( `[SessionManager] Session ${sessionIdToLog} is already being cleaned up, skipping.\n`, ); return; } // Mark session as being cleaned up this.cleaningUpSessions.add(sessionIdToLog); try { // Close Stagehand instance which handles browser cleanup if (session?.stagehand) { try { process.stderr.write( `[SessionManager] Closing Stagehand for session: ${sessionIdToLog}\n`, ); await session.stagehand.close(); 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}: ${ closeError instanceof Error ? closeError.message : String(closeError) }\n`, ); } } } finally { // Always remove from cleanup tracking set this.cleaningUpSessions.delete(sessionIdToLog); } } // Internal function to ensure default session // Uses a mutex pattern to prevent race conditions when multiple calls happen concurrently async ensureDefaultSessionInternal(config: Config): Promise<BrowserSession> { // If a creation is already in progress, wait for it instead of starting a new one if (this.defaultSessionCreationPromise) { process.stderr.write( `[SessionManager] Default session creation already in progress, waiting...\n`, ); return await this.defaultSessionCreationPromise; } const sessionId = this.defaultSessionId; let needsReCreation = false; if (!this.defaultBrowserSession) { needsReCreation = true; process.stderr.write( `[SessionManager] Default session ${sessionId} not found, creating.\n`, ); } else if ( !this.defaultBrowserSession.browser.isConnected() || this.defaultBrowserSession.page.isClosed() ) { needsReCreation = true; process.stderr.write( `[SessionManager] Default session ${sessionId} is stale, recreating.\n`, ); await this.closeBrowserGracefully(this.defaultBrowserSession, sessionId); this.defaultBrowserSession = null; this.browsers.delete(sessionId); } if (needsReCreation) { // Set the mutex promise before starting creation this.defaultSessionCreationPromise = (async () => { try { this.defaultBrowserSession = await this.createNewBrowserSession( sessionId, config, ); return this.defaultBrowserSession; } catch (creationError) { // Error during initial creation or recreation process.stderr.write( `[SessionManager] Initial/Recreation attempt for default session ${sessionId} failed. Error: ${ creationError instanceof Error ? creationError.message : String(creationError) }\n`, ); // Attempt one more time after a failure process.stderr.write( `[SessionManager] Retrying creation of default session ${sessionId} after error...\n`, ); try { this.defaultBrowserSession = await this.createNewBrowserSession( sessionId, config, ); return this.defaultBrowserSession; } catch (retryError) { const finalErrorMessage = retryError instanceof Error ? retryError.message : String(retryError); process.stderr.write( `[SessionManager] Failed to recreate default session ${sessionId} after retry: ${finalErrorMessage}\n`, ); throw new Error( `Failed to ensure default session ${sessionId} after initial error and retry: ${finalErrorMessage}`, ); } } finally { // Clear the mutex after creation completes or fails this.defaultSessionCreationPromise = null; } })(); return await this.defaultSessionCreationPromise; } // If we reached here, the existing default session is considered okay. this.setActiveSessionId(sessionId); // Ensure default is marked active return this.defaultBrowserSession!; // Non-null assertion: logic ensures it's not null here } // Get a specific session by ID async getSession( sessionId: string, config: Config, createIfMissing: boolean = true, ): Promise<BrowserSession | null> { if (sessionId === this.defaultSessionId && createIfMissing) { try { return await this.ensureDefaultSessionInternal(config); } catch { process.stderr.write( `[SessionManager] Failed to get default session due to error in ensureDefaultSessionInternal for ${sessionId}. See previous messages for details.\n`, ); return null; } } // For non-default sessions process.stderr.write(`[SessionManager] Getting session: ${sessionId}\n`); const sessionObj = this.browsers.get(sessionId); if (!sessionObj) { process.stderr.write( `[SessionManager] WARN - Session not found in map: ${sessionId}\n`, ); return null; } // Validate the found session if (!sessionObj.browser.isConnected() || sessionObj.page.isClosed()) { process.stderr.write( `[SessionManager] WARN - Found session ${sessionId} is stale, removing.\n`, ); await this.closeBrowserGracefully(sessionObj, sessionId); this.browsers.delete(sessionId); if (this.activeSessionId === sessionId) { process.stderr.write( `[SessionManager] WARN - Invalidated active session ${sessionId}, resetting to default.\n`, ); this.setActiveSessionId(this.defaultSessionId); } return null; } // Session appears valid, make it active this.setActiveSessionId(sessionId); process.stderr.write( `[SessionManager] Using valid session: ${sessionId}\n`, ); return sessionObj; } /** * Clean up a session by closing the browser and removing it from tracking. * This method handles both closing Stagehand and cleanup, and is idempotent. * * @param sessionId The session ID to clean up */ async cleanupSession(sessionId: string): Promise<void> { process.stderr.write( `[SessionManager] Cleaning up session: ${sessionId}\n`, ); // Get the session to close it gracefully const session = this.browsers.get(sessionId); if (session) { await this.closeBrowserGracefully(session, sessionId); } // Remove from browsers map this.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 === this.defaultSessionId && this.defaultBrowserSession) { this.defaultBrowserSession = null; } // Reset active session to default if this was the active one if (this.activeSessionId === sessionId) { process.stderr.write( `[SessionManager] Cleaned up active session ${sessionId}, resetting to default.\n`, ); this.setActiveSessionId(this.defaultSessionId); } } // Function to close all managed browser sessions gracefully async closeAllSessions(): Promise<void> { process.stderr.write(`[SessionManager] Closing all sessions...\n`); const closePromises: Promise<void>[] = []; for (const [id, session] of this.browsers.entries()) { process.stderr.write(`[SessionManager] Closing session: ${id}\n`); closePromises.push( // Use the helper for consistent logging/error handling this.closeBrowserGracefully(session, id), ); } try { await Promise.all(closePromises); } catch { // Individual errors are caught and logged by closeBrowserGracefully process.stderr.write( `[SessionManager] WARN - Some errors occurred during batch session closing. See individual messages.\n`, ); } this.browsers.clear(); this.defaultBrowserSession = null; this.setActiveSessionId(this.defaultSessionId); // Reset active session to default process.stderr.write(`[SessionManager] All sessions closed and cleared.\n`); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/browserbase/mcp-server-browserbase'

If you have feedback or need assistance with the MCP directory API, please join our Discord server