#
tokens: 39056/50000 45/45 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .changeset
│   ├── config.json
│   └── README.md
├── .dockerignore
├── .gitattributes
├── .github
│   └── workflows
│       ├── ci.yml
│       └── publish-mcp.yml
├── .gitignore
├── .husky
│   └── pre-commit
├── .npmrc
├── assets
│   ├── browserbase-mcp.png
│   ├── cover.png
│   └── smithery.jpg
├── CHANGELOG.md
├── cli.js
├── config.d.ts
├── Dockerfile
├── eslint.config.js
├── evals
│   ├── mcp-eval-basic.config.json
│   ├── mcp-eval-minimal.config.json
│   ├── mcp-eval.config.json
│   └── run-evals.ts
├── index.d.ts
├── index.js
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── server.json
├── smithery.config.js
├── smithery.yaml
├── src
│   ├── config.ts
│   ├── context.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── resources.ts
│   │   └── sampling.ts
│   ├── program.ts
│   ├── server.ts
│   ├── sessionManager.ts
│   ├── tools
│   │   ├── act.ts
│   │   ├── extract.ts
│   │   ├── index.ts
│   │   ├── navigate.ts
│   │   ├── observe.ts
│   │   ├── screenshot.ts
│   │   ├── session.ts
│   │   ├── tool.ts
│   │   └── url.ts
│   ├── transport.ts
│   └── types
│       └── types.ts
├── tests
│   └── .gitkeep
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/tests/.gitkeep:
--------------------------------------------------------------------------------

```
1 | 
```

--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------

```
1 | package-lock.json linguist-generated=true
2 | 
```

--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------

```
1 | # Use npm for package management
2 | engine-strict=true
3 | 
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | 
 4 | # Build output
 5 | dist/
 6 | 
 7 | # Tests
 8 | tests/
 9 | evals/
10 | 
11 | # Git
12 | .git/
13 | .gitignore
14 | 
15 | # Documentation
16 | *.md
17 | !README.md
18 | assets/
19 | 
20 | # CI/CD
21 | .github/
22 | 
23 | # IDE
24 | .vscode/
25 | .idea/
26 | 
27 | # Logs
28 | *.log
29 | npm-debug.log*
30 | 
31 | # Environment
32 | .env
33 | .env.local
34 | 
35 | # Misc
36 | .DS_Store
37 | *.swp
38 | *.swo
39 | *~
40 | 
41 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | dist
 93 | 
 94 | # Gatsby files
 95 | .cache/
 96 | # Comment in the public line in if your project uses Gatsby and not Next.js
 97 | # https://nextjs.org/blog/next-9-1#public-directory-support
 98 | # public
 99 | 
100 | # vuepress build output
101 | .vuepress/dist
102 | 
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 | 
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 | 
110 | # Serverless directories
111 | .serverless/
112 | 
113 | # FuseBox cache
114 | .fusebox/
115 | 
116 | # DynamoDB Local files
117 | .dynamodb/
118 | 
119 | # TernJS port file
120 | .tern-port
121 | 
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 | 
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 | 
132 | build/
133 | 
134 | gcp-oauth.keys.json
135 | .*-server-credentials.json
136 | 
137 | # Byte-compiled / optimized / DLL files
138 | __pycache__/
139 | *.py[cod]
140 | *$py.class
141 | 
142 | # C extensions
143 | *.so
144 | 
145 | # Distribution / packaging
146 | .Python
147 | build/
148 | develop-eggs/
149 | dist/
150 | downloads/
151 | eggs/
152 | .eggs/
153 | lib/
154 | lib64/
155 | parts/
156 | sdist/
157 | var/
158 | wheels/
159 | share/python-wheels/
160 | *.egg-info/
161 | .installed.cfg
162 | *.egg
163 | MANIFEST
164 | 
165 | # PyInstaller
166 | #  Usually these files are written by a python script from a template
167 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
168 | *.manifest
169 | *.spec
170 | 
171 | # Installer logs
172 | pip-log.txt
173 | pip-delete-this-directory.txt
174 | 
175 | # Unit test / coverage reports
176 | htmlcov/
177 | .tox/
178 | .nox/
179 | .coverage
180 | .coverage.*
181 | .cache
182 | nosetests.xml
183 | coverage.xml
184 | *.cover
185 | *.py,cover
186 | .hypothesis/
187 | .pytest_cache/
188 | cover/
189 | 
190 | # Translations
191 | *.mo
192 | *.pot
193 | 
194 | # Django stuff:
195 | *.log
196 | local_settings.py
197 | db.sqlite3
198 | db.sqlite3-journal
199 | 
200 | # Flask stuff:
201 | instance/
202 | .webassets-cache
203 | 
204 | # Scrapy stuff:
205 | .scrapy
206 | 
207 | # Sphinx documentation
208 | docs/_build/
209 | 
210 | # PyBuilder
211 | .pybuilder/
212 | target/
213 | 
214 | # Jupyter Notebook
215 | .ipynb_checkpoints
216 | 
217 | # IPython
218 | profile_default/
219 | ipython_config.py
220 | 
221 | # pyenv
222 | #   For a library or package, you might want to ignore these files since the code is
223 | #   intended to run in multiple environments; otherwise, check them in:
224 | # .python-version
225 | 
226 | # pipenv
227 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
228 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
229 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
230 | #   install all needed dependencies.
231 | #Pipfile.lock
232 | 
233 | # poetry
234 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
235 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
236 | #   commonly ignored for libraries.
237 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
238 | #poetry.lock
239 | 
240 | # pdm
241 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
242 | #pdm.lock
243 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
244 | #   in version control.
245 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
246 | .pdm.toml
247 | .pdm-python
248 | .pdm-build/
249 | 
250 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
251 | __pypackages__/
252 | 
253 | # Celery stuff
254 | celerybeat-schedule
255 | celerybeat.pid
256 | 
257 | # SageMath parsed files
258 | *.sage.py
259 | 
260 | # Environments
261 | .env
262 | .venv
263 | env/
264 | venv/
265 | ENV/
266 | env.bak/
267 | venv.bak/
268 | 
269 | # Spyder project settings
270 | .spyderproject
271 | .spyproject
272 | 
273 | # Rope project settings
274 | .ropeproject
275 | 
276 | # mkdocs documentation
277 | /site
278 | 
279 | # mypy
280 | .mypy_cache/
281 | .dmypy.json
282 | dmypy.json
283 | 
284 | # Pyre type checker
285 | .pyre/
286 | 
287 | # pytype static type analyzer
288 | .pytype/
289 | 
290 | # Cython debug symbols
291 | cython_debug/
292 | 
293 | .DS_Store
294 | 
295 | # Smithery
296 | /.smithery
297 | 
298 | # MCP Registry https://github.com/modelcontextprotocol/registry
299 | .mcpregistry_github_token
300 | .mcpregistry_registry_token
```

--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------

```markdown
1 | # Changesets
2 | 
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 | 
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Browserbase MCP Server
  2 | 
  3 | [![smithery badge](https://smithery.ai/badge/@browserbasehq/mcp-browserbase)](https://smithery.ai/server/@browserbasehq/mcp-browserbase)
  4 | 
  5 | ![cover](assets/cover.png)
  6 | 
  7 | [The Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you're building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need.
  8 | 
  9 | This server provides cloud browser automation capabilities using [Browserbase](https://www.browserbase.com/) and [Stagehand](https://github.com/browserbase/stagehand). It enables LLMs to interact with web pages, take screenshots, extract information, and perform automated actions with atomic precision.
 10 | 
 11 | ## Features
 12 | 
 13 | | Feature            | Description                                                 |
 14 | | ------------------ | ----------------------------------------------------------- |
 15 | | Browser Automation | Control and orchestrate cloud browsers via Browserbase      |
 16 | | Data Extraction    | Extract structured data from any webpage                    |
 17 | | Web Interaction    | Navigate, click, and fill forms with ease                   |
 18 | | Screenshots        | Capture full-page and element screenshots                   |
 19 | | Model Flexibility  | Supports multiple models (OpenAI, Claude, Gemini, and more) |
 20 | | Vision Support     | Use annotated screenshots for complex DOMs                  |
 21 | | Session Management | Create, manage, and close browser sessions                  |
 22 | 
 23 | ## How to Setup
 24 | 
 25 | ### Quickstarts:
 26 | 
 27 | #### Add to Cursor
 28 | 
 29 | Copy and Paste this link in your Browser:
 30 | 
 31 | ```text
 32 | cursor://anysphere.cursor-deeplink/mcp/install?name=browserbase&config=eyJjb21tYW5kIjoibnB4IEBicm93c2VyYmFzZWhxL21jcCIsImVudiI6eyJCUk9XU0VSQkFTRV9BUElfS0VZIjoiIiwiQlJPV1NFUkJBU0VfUFJPSkVDVF9JRCI6IiIsIkdFTUlOSV9BUElfS0VZIjoiIn19
 33 | ```
 34 | 
 35 | We currently support 2 transports for our MCP server, STDIO and SHTTP. We recommend you use SHTTP with our remote hosted url to take advantage of the server at full capacity.
 36 | 
 37 | ## SHTTP:
 38 | 
 39 | To use the Browserbase MCP Server through our remote hosted URL, add the following to your configuration.
 40 | 
 41 | Go to [smithery.ai](https://smithery.ai/server/@browserbasehq/mcp-browserbase) and enter your API keys and configuration to get a remote hosted URL.
 42 | When using our remote hosted server, we provide the LLM costs for Gemini, the [best performing model](https://www.stagehand.dev/evals) in [Stagehand](https://www.stagehand.dev).
 43 | 
 44 | ![Smithery Image](assets/smithery.jpg)
 45 | 
 46 | If your client supports SHTTP:
 47 | 
 48 | ```json
 49 | {
 50 |   "mcpServers": {
 51 |     "browserbase": {
 52 |       "url": "your-smithery-url.com"
 53 |     }
 54 |   }
 55 | }
 56 | ```
 57 | 
 58 | If your client doesn't support SHTTP:
 59 | 
 60 | ```json
 61 | {
 62 |   "mcpServers": {
 63 |     "browserbase": {
 64 |       "command": "npx",
 65 |       "args": ["mcp-remote", "your-smithery-url.com"]
 66 |     }
 67 |   }
 68 | }
 69 | ```
 70 | 
 71 | ## STDIO:
 72 | 
 73 | You can either use our Server hosted on NPM or run it completely locally by cloning this repo.
 74 | 
 75 | > **❗️ Important:** If you want to use a different model you have to add --modelName to the args and provide that respective key as an arg. More info below.
 76 | 
 77 | ### To run on NPM (Recommended)
 78 | 
 79 | Go into your MCP Config JSON and add the Browserbase Server:
 80 | 
 81 | ```json
 82 | {
 83 |   "mcpServers": {
 84 |     "browserbase": {
 85 |       "command": "npx",
 86 |       "args": ["@browserbasehq/mcp-server-browserbase"],
 87 |       "env": {
 88 |         "BROWSERBASE_API_KEY": "",
 89 |         "BROWSERBASE_PROJECT_ID": "",
 90 |         "GEMINI_API_KEY": ""
 91 |       }
 92 |     }
 93 |   }
 94 | }
 95 | ```
 96 | 
 97 | That's it! Reload your MCP client and Claude will be able to use Browserbase.
 98 | 
 99 | ### To run 100% local:
100 | 
101 | #### Option 1: Direct installation
102 | 
103 | ```bash
104 | # Clone the Repo
105 | git clone https://github.com/browserbase/mcp-server-browserbase.git
106 | cd mcp-server-browserbase
107 | 
108 | # Install the dependencies and build the project
109 | npm install && npm run build
110 | ```
111 | 
112 | #### Option 2: Docker
113 | 
114 | ```bash
115 | # Clone the Repo
116 | git clone https://github.com/browserbase/mcp-server-browserbase.git
117 | cd mcp-server-browserbase
118 | 
119 | # Build the Docker image
120 | docker build -t mcp-browserbase .
121 | ```
122 | 
123 | Then in your MCP Config JSON run the server. To run locally we can use STDIO or self-host SHTTP.
124 | 
125 | ### STDIO:
126 | 
127 | #### Using Direct Installation
128 | 
129 | To your MCP Config JSON file add the following:
130 | 
131 | ```json
132 | {
133 |   "mcpServers": {
134 |     "browserbase": {
135 |       "command": "node",
136 |       "args": ["/path/to/mcp-server-browserbase/cli.js"],
137 |       "env": {
138 |         "BROWSERBASE_API_KEY": "",
139 |         "BROWSERBASE_PROJECT_ID": "",
140 |         "GEMINI_API_KEY": ""
141 |       }
142 |     }
143 |   }
144 | }
145 | ```
146 | 
147 | #### Using Docker
148 | 
149 | To your MCP Config JSON file add the following:
150 | 
151 | ```json
152 | {
153 |   "mcpServers": {
154 |     "browserbase": {
155 |       "command": "docker",
156 |       "args": [
157 |         "run",
158 |         "--rm",
159 |         "-i",
160 |         "-e",
161 |         "BROWSERBASE_API_KEY",
162 |         "-e",
163 |         "BROWSERBASE_PROJECT_ID",
164 |         "-e",
165 |         "GEMINI_API_KEY",
166 |         "mcp-browserbase"
167 |       ],
168 |       "env": {
169 |         "BROWSERBASE_API_KEY": "",
170 |         "BROWSERBASE_PROJECT_ID": "",
171 |         "GEMINI_API_KEY": ""
172 |       }
173 |     }
174 |   }
175 | }
176 | ```
177 | 
178 | Then reload your MCP client and you should be good to go!
179 | 
180 | ## Configuration
181 | 
182 | The Browserbase MCP server accepts the following command-line flags:
183 | 
184 | | Flag                       | Description                                                                 |
185 | | -------------------------- | --------------------------------------------------------------------------- |
186 | | `--proxies`                | Enable Browserbase proxies for the session                                  |
187 | | `--advancedStealth`        | Enable Browserbase Advanced Stealth (Only for Scale Plan Users)             |
188 | | `--keepAlive`              | Enable Browserbase Keep Alive Session                                       |
189 | | `--contextId <contextId>`  | Specify a Browserbase Context ID to use                                     |
190 | | `--persist`                | Whether to persist the Browserbase context (default: true)                  |
191 | | `--port <port>`            | Port to listen on for HTTP/SHTTP transport                                  |
192 | | `--host <host>`            | Host to bind server to (default: localhost, use 0.0.0.0 for all interfaces) |
193 | | `--cookies [json]`         | JSON array of cookies to inject into the browser                            |
194 | | `--browserWidth <width>`   | Browser viewport width (default: 1024)                                      |
195 | | `--browserHeight <height>` | Browser viewport height (default: 768)                                      |
196 | | `--modelName <model>`      | The model to use for Stagehand (default: gemini-2.0-flash)                  |
197 | | `--modelApiKey <key>`      | API key for the custom model provider (required when using custom models)   |
198 | | `--experimental`           | Enable experimental features (default: false)                               |
199 | 
200 | These flags can be passed directly to the CLI or configured in your MCP configuration file.
201 | 
202 | ### NOTE:
203 | 
204 | Currently, these flags can only be used with the local server (npx @browserbasehq/mcp-server-browserbase or Docker).
205 | 
206 | ### Using Configuration Flags with Docker
207 | 
208 | When using Docker, you can pass configuration flags as additional arguments after the image name. Here's an example with the `--proxies` flag:
209 | 
210 | ```json
211 | {
212 |   "mcpServers": {
213 |     "browserbase": {
214 |       "command": "docker",
215 |       "args": [
216 |         "run",
217 |         "--rm",
218 |         "-i",
219 |         "-e",
220 |         "BROWSERBASE_API_KEY",
221 |         "-e",
222 |         "BROWSERBASE_PROJECT_ID",
223 |         "-e",
224 |         "GEMINI_API_KEY",
225 |         "mcp-browserbase",
226 |         "--proxies"
227 |       ],
228 |       "env": {
229 |         "BROWSERBASE_API_KEY": "",
230 |         "BROWSERBASE_PROJECT_ID": "",
231 |         "GEMINI_API_KEY": ""
232 |       }
233 |     }
234 |   }
235 | }
236 | ```
237 | 
238 | You can also run the Docker container directly from the command line:
239 | 
240 | ```bash
241 | docker run --rm -i \
242 |   -e BROWSERBASE_API_KEY=your_api_key \
243 |   -e BROWSERBASE_PROJECT_ID=your_project_id \
244 |   -e GEMINI_API_KEY=your_gemini_key \
245 |   mcp-browserbase --proxies
246 | ```
247 | 
248 | ## Configuration Examples
249 | 
250 | ### Proxies
251 | 
252 | Here are our docs on [Proxies](https://docs.browserbase.com/features/proxies).
253 | 
254 | To use proxies, set the --proxies flag in your MCP Config:
255 | 
256 | ```json
257 | {
258 |   "mcpServers": {
259 |     "browserbase": {
260 |       "command": "npx",
261 |       "args": ["@browserbasehq/mcp-server-browserbase", "--proxies"],
262 |       "env": {
263 |         "BROWSERBASE_API_KEY": "",
264 |         "BROWSERBASE_PROJECT_ID": "",
265 |         "GEMINI_API_KEY": ""
266 |       }
267 |     }
268 |   }
269 | }
270 | ```
271 | 
272 | ### Advanced Stealth
273 | 
274 | Here are our docs on [Advanced Stealth](https://docs.browserbase.com/features/stealth-mode#advanced-stealth-mode).
275 | 
276 | To use advanced stealth, set the --advancedStealth flag in your MCP Config:
277 | 
278 | ```json
279 | {
280 |   "mcpServers": {
281 |     "browserbase": {
282 |       "command": "npx",
283 |       "args": ["@browserbasehq/mcp-server-browserbase", "--advancedStealth"],
284 |       "env": {
285 |         "BROWSERBASE_API_KEY": "",
286 |         "BROWSERBASE_PROJECT_ID": "",
287 |         "GEMINI_API_KEY": ""
288 |       }
289 |     }
290 |   }
291 | }
292 | ```
293 | 
294 | ### Contexts
295 | 
296 | Here are our docs on [Contexts](https://docs.browserbase.com/features/contexts)
297 | 
298 | To use contexts, set the --contextId flag in your MCP Config:
299 | 
300 | ```json
301 | {
302 |   "mcpServers": {
303 |     "browserbase": {
304 |       "command": "npx",
305 |       "args": [
306 |         "@browserbasehq/mcp-server-browserbase",
307 |         "--contextId",
308 |         "<YOUR_CONTEXT_ID>"
309 |       ],
310 |       "env": {
311 |         "BROWSERBASE_API_KEY": "",
312 |         "BROWSERBASE_PROJECT_ID": "",
313 |         "GEMINI_API_KEY": ""
314 |       }
315 |     }
316 |   }
317 | }
318 | ```
319 | 
320 | ### Browser Viewport Sizing
321 | 
322 | The default viewport sizing for a browser session is 1024 x 768. You can adjust the Browser viewport sizing with browserWidth and browserHeight flags.
323 | 
324 | Here's how to use it for custom browser sizing. We recommend to stick with 16:9 aspect ratios (ie: 1920 x 1080, 1280 x 720, 1024 x 768)
325 | 
326 | ```json
327 | {
328 |   "mcpServers": {
329 |     "browserbase": {
330 |       "command": "npx",
331 |       "args": [
332 |         "@browserbasehq/mcp-server-browserbase",
333 |         "--browserHeight 1080",
334 |         "--browserWidth 1920"
335 |       ],
336 |       "env": {
337 |         "BROWSERBASE_API_KEY": "",
338 |         "BROWSERBASE_PROJECT_ID": "",
339 |         "GEMINI_API_KEY": ""
340 |       }
341 |     }
342 |   }
343 | }
344 | ```
345 | 
346 | ### Model Configuration
347 | 
348 | Stagehand defaults to using Google's Gemini 2.0 Flash model, but you can configure it to use other models like GPT-4o, Claude, or other providers.
349 | 
350 | **Important**: When using any custom model (non-default), you must provide your own API key for that model provider using the `--modelApiKey` flag.
351 | 
352 | Here's how to configure different models:
353 | 
354 | ```json
355 | {
356 |   "mcpServers": {
357 |     "browserbase": {
358 |       "command": "npx",
359 |       "args": [
360 |         "@browserbasehq/mcp-server-browserbase",
361 |         "--modelName",
362 |         "anthropic/claude-3-5-sonnet-latest",
363 |         "--modelApiKey",
364 |         "your-anthropic-api-key"
365 |       ],
366 |       "env": {
367 |         "BROWSERBASE_API_KEY": "",
368 |         "BROWSERBASE_PROJECT_ID": ""
369 |       }
370 |     }
371 |   }
372 | }
373 | ```
374 | 
375 | _Note: The model must be supported in Stagehand. Check out the docs [here](https://docs.stagehand.dev/examples/custom_llms#supported-llms). When using any custom model, you must provide your own API key for that provider._
376 | 
377 | ### Resources
378 | 
379 | The server provides access to screenshot resources:
380 | 
381 | 1. **Screenshots** (`screenshot://<screenshot-name>`)
382 |    - PNG images of captured screenshots
383 | 
384 | ## Key Features
385 | 
386 | - **AI-Powered Automation**: Natural language commands for web interactions
387 | - **Multi-Model Support**: Works with OpenAI, Claude, Gemini, and more
388 | - **Screenshot Capture**: Full-page and element-specific screenshots
389 | - **Data Extraction**: Intelligent content extraction from web pages
390 | - **Proxy Support**: Enterprise-grade proxy capabilities
391 | - **Stealth Mode**: Advanced anti-detection features
392 | - **Context Persistence**: Maintain authentication and state across sessions
393 | 
394 | For more information about the Model Context Protocol, visit:
395 | 
396 | - [MCP Documentation](https://modelcontextprotocol.io/docs)
397 | - [MCP Specification](https://spec.modelcontextprotocol.io/)
398 | 
399 | For the official MCP Docs:
400 | 
401 | - [Browserbase MCP](https://docs.browserbase.com/integrations/mcp/introduction)
402 | 
403 | ## License
404 | 
405 | Licensed under the Apache 2.0 License.
406 | 
407 | Copyright 2025 Browserbase, Inc.
408 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
1 | runtime: "typescript"
2 | 
```

--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------

```javascript
1 | #!/usr/bin/env node
2 | import "./dist/program.js";
3 | 
```

--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------

```javascript
1 | import { createServer } from "./dist/index.js";
2 | export default { createServer };
3 | 
```

--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------

```typescript
1 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | 
3 | import type { Config } from "./config";
4 | 
5 | export declare function createServer(config?: Config): Promise<Server>;
6 | export {};
7 | 
```

--------------------------------------------------------------------------------
/smithery.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | /**
 2 |  * @type {import('esbuild').BuildOptions}
 3 |  */
 4 | export default {
 5 |   esbuild: {
 6 |     // Mark playwright-core as external to prevent bundling
 7 |     // This avoids the relative path resolution issue in Docker
 8 |     external: ["playwright-core"],
 9 |   },
10 | };
11 | 
```

--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
 3 |   "changelog": "@changesets/cli/changelog",
 4 |   "commit": false,
 5 |   "fixed": [],
 6 |   "linked": [],
 7 |   "access": "public",
 8 |   "baseBranch": "main",
 9 |   "updateInternalDependencies": "patch",
10 |   "ignore": []
11 | }
12 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "bundler",
 6 |     "strict": true,
 7 |     "esModuleInterop": true,
 8 |     "skipLibCheck": true,
 9 |     "forceConsistentCasingInFileNames": true,
10 |     "resolveJsonModule": true,
11 |     "outDir": "dist",
12 |     "rootDir": "src",
13 |     "noErrorTruncation": false
14 |   },
15 |   "include": ["src/**/*.ts"],
16 |   "exclude": ["node_modules"]
17 | }
18 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # @browserbasehq/mcp-server-browserbase
 2 | 
 3 | ## 2.2.0
 4 | 
 5 | ### Minor Changes
 6 | 
 7 | - Remove multisession tools, remove prompts sampling, simplify tool descriptions for better context, add support if google apikey set, latest version of stagehand, remove custom availmodelschema to use stagehand model type instead.
 8 | 
 9 | ## 2.1.3
10 | 
11 | ### Patch Changes
12 | 
13 | - Adding docker deployment support
14 | 
15 | ## 2.1.2
16 | 
17 | ### Patch Changes
18 | 
19 | - fixing screenshot map behavior
20 | 
21 | ## 2.1.1
22 | 
23 | ### Patch Changes
24 | 
25 | - adding MCP server to official registry
26 | 
27 | ## 2.1.0
28 | 
29 | ### Minor Changes
30 | 
31 | - adding changesets, MCP UI for session create
32 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:22-alpine AS builder
 2 | 
 3 | RUN corepack enable
 4 | 
 5 | WORKDIR /app
 6 | 
 7 | COPY package.json pnpm-lock.yaml ./
 8 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
 9 |     pnpm install --frozen-lockfile --ignore-scripts
10 | 
11 | COPY . .
12 | RUN pnpm run build && \
13 |     pnpm prune --prod --ignore-scripts
14 | 
15 | FROM gcr.io/distroless/nodejs22-debian12
16 | 
17 | LABEL io.modelcontextprotocol.server.name="io.github.browserbase/mcp-server-browserbase"
18 | 
19 | WORKDIR /app
20 | 
21 | COPY --from=builder /app/package.json ./
22 | COPY --from=builder /app/node_modules ./node_modules
23 | COPY --from=builder /app/dist ./dist
24 | COPY --from=builder /app/cli.js ./cli.js
25 | COPY --from=builder /app/index.js ./index.js
26 | 
27 | CMD ["cli.js"]
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
 2 | 
 3 | export class ServerList {
 4 |   private _servers: Server[] = [];
 5 |   private _serverFactory: () => Promise<Server>;
 6 | 
 7 |   constructor(serverFactory: () => Promise<Server>) {
 8 |     this._serverFactory = serverFactory;
 9 |   }
10 | 
11 |   async create() {
12 |     const server = await this._serverFactory();
13 |     this._servers.push(server);
14 |     return server;
15 |   }
16 | 
17 |   async close(server: Server) {
18 |     await server.close();
19 |     const index = this._servers.indexOf(server);
20 |     if (index !== -1) this._servers.splice(index, 1);
21 |   }
22 | 
23 |   async closeAll() {
24 |     await Promise.all(this._servers.map((server) => server.close()));
25 |   }
26 | }
27 | 
```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | import js from "@eslint/js";
 2 | import globals from "globals";
 3 | import tseslint from "typescript-eslint";
 4 | import { defineConfig } from "eslint/config";
 5 | 
 6 | export default defineConfig([
 7 |   {
 8 |     files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
 9 |     plugins: { js },
10 |     extends: ["js/recommended"],
11 |     ignores: ["dist/**/*"],
12 |   },
13 |   {
14 |     files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
15 |     languageOptions: { globals: { ...globals.browser, ...globals.node } },
16 |     ignores: ["dist/**/*"],
17 |   },
18 |   ...tseslint.configs.recommended,
19 |   {
20 |     files: ["src/types/**/*.ts", "src/mcp/**/*.ts"],
21 |     rules: {
22 |       "@typescript-eslint/no-explicit-any": "off",
23 |       "@typescript-eslint/no-unused-vars": "off",
24 |       "@typescript-eslint/ban-ts-comment": "off",
25 |     },
26 |   },
27 |   {
28 |     ignores: ["dist/**/*", "node_modules/**/*", ".smithery/**/*"],
29 |   },
30 | ]);
31 | 
```

--------------------------------------------------------------------------------
/src/tools/tool.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type {
 2 |   ImageContent,
 3 |   TextContent,
 4 | } from "@modelcontextprotocol/sdk/types.js";
 5 | import type { z } from "zod";
 6 | import type { Context } from "../context.js";
 7 | 
 8 | export type ToolSchema<Input extends InputType> = {
 9 |   name: string;
10 |   description: string;
11 |   inputSchema: Input;
12 | };
13 | 
14 | // Export InputType
15 | export type InputType = z.Schema;
16 | 
17 | export type ToolActionResult =
18 |   | { content?: (ImageContent | TextContent)[] }
19 |   | undefined
20 |   | void;
21 | 
22 | export type ToolResult = {
23 |   action?: () => Promise<ToolActionResult>;
24 |   waitForNetwork: boolean;
25 | };
26 | 
27 | export type Tool<Input extends InputType = InputType> = {
28 |   capability: string;
29 |   schema: ToolSchema<Input>;
30 |   handle: (context: Context, params: z.output<Input>) => Promise<ToolResult>;
31 | };
32 | 
33 | export function defineTool<Input extends InputType>(
34 |   tool: Tool<Input>,
35 | ): Tool<Input> {
36 |   return tool;
37 | }
38 | 
39 | export {}; // Ensure this is treated as a module
40 | 
```

--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import navigateTool from "./navigate.js";
 2 | import actTool from "./act.js";
 3 | import extractTool from "./extract.js";
 4 | import observeTool from "./observe.js";
 5 | import screenshotTool from "./screenshot.js";
 6 | import sessionTools from "./session.js";
 7 | import getUrlTool from "./url.js";
 8 | 
 9 | // Export individual tools
10 | export { default as navigateTool } from "./navigate.js";
11 | export { default as actTool } from "./act.js";
12 | export { default as extractTool } from "./extract.js";
13 | export { default as observeTool } from "./observe.js";
14 | export { default as screenshotTool } from "./screenshot.js";
15 | export { default as sessionTools } from "./session.js";
16 | export { default as getUrlTool } from "./url.js";
17 | 
18 | // Export all tools as array
19 | export const TOOLS = [
20 |   ...sessionTools,
21 |   navigateTool,
22 |   actTool,
23 |   extractTool,
24 |   observeTool,
25 |   screenshotTool,
26 |   getUrlTool,
27 | ];
28 | 
29 | export const sessionManagementTools = sessionTools;
30 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   pull_request:
 5 |     branches: [main]
 6 |   push:
 7 |     branches: [main]
 8 | 
 9 | jobs:
10 |   test:
11 |     name: Test and Lint
12 |     runs-on: ubuntu-latest
13 | 
14 |     steps:
15 |       - name: Checkout code
16 |         uses: actions/checkout@v4
17 | 
18 |       - name: Install pnpm
19 |         uses: pnpm/action-setup@v4
20 | 
21 |       - name: Setup Node.js
22 |         uses: actions/setup-node@v4
23 |         with:
24 |           node-version: "22"
25 |           cache: "pnpm"
26 | 
27 |       - name: Install dependencies
28 |         run: pnpm install --frozen-lockfile
29 | 
30 |       - name: Run linting
31 |         run: pnpm lint
32 | 
33 |       - name: Check formatting
34 |         run: pnpm format
35 | 
36 |       - name: Build project
37 |         run: pnpm build
38 | 
39 |       - name: Run evaluation tests
40 |         env:
41 |           BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }}
42 |           BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }}
43 |           ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
44 |           GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
45 |         run: |
46 |           pnpm evals
47 | 
```

--------------------------------------------------------------------------------
/src/mcp/sampling.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Sampling module for the Browserbase MCP server
 3 |  * Implements sampling capability to request LLM completions from clients
 4 |  * Docs: https://modelcontextprotocol.io/docs/concepts/sampling
 5 |  */
 6 | 
 7 | /**
 8 |  * Sampling capability configuration
 9 |  * This indicates that the server can request LLM completions
10 |  */
11 | export const SAMPLING_CAPABILITY = {};
12 | 
13 | /**
14 |  * Note: Sampling in MCP is initiated BY the server TO the client.
15 |  * The server sends sampling/createMessage requests to ask the client
16 |  * for LLM completions. This is useful for intelligent browser automation
17 |  * where the server needs AI assistance to analyze pages and make decisions.
18 |  *
19 |  * Currently, sampling support depends on the MCP client implementation.
20 |  * Not all clients support sampling yet. (ie claude desktop)
21 |  */
22 | 
23 | /**
24 |  * Type definitions for sampling messages
25 |  */
26 | export type SamplingMessage = {
27 |   role: "user" | "assistant";
28 |   content: {
29 |     type: "text" | "image";
30 |     text?: string;
31 |     data?: string; // base64 for images
32 |     mimeType?: string;
33 |   };
34 | };
35 | 
```

--------------------------------------------------------------------------------
/src/types/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type { Stagehand, Browser, Page } from "@browserbasehq/stagehand";
 2 | import { ImageContent, TextContent } from "@modelcontextprotocol/sdk/types.js";
 3 | import { Tool } from "../tools/tool.js";
 4 | import { InputType } from "../tools/tool.js";
 5 | 
 6 | export type StagehandSession = {
 7 |   id: string; // MCP-side ID
 8 |   stagehand: Stagehand; // owns the Browserbase session
 9 |   page: Page;
10 |   browser: Browser;
11 |   created: number;
12 |   metadata?: Record<string, any>; // optional extras (proxy, contextId, bbSessionId)
13 | };
14 | 
15 | export type CreateSessionParams = {
16 |   apiKey?: string;
17 |   projectId?: string;
18 |   modelName?: string;
19 |   modelApiKey?: string;
20 |   browserbaseSessionID?: string;
21 |   browserbaseSessionCreateParams?: any;
22 |   meta?: Record<string, any>;
23 | };
24 | 
25 | export type BrowserSession = {
26 |   browser: Browser;
27 |   page: Page;
28 |   sessionId: string;
29 |   stagehand: Stagehand;
30 | };
31 | 
32 | export type ToolActionResult =
33 |   | { content?: (ImageContent | TextContent)[] }
34 |   | undefined
35 |   | void;
36 | 
37 | // Type for the tools array used in MCP server registration
38 | export type MCPTool = Tool<InputType>;
39 | export type MCPToolsArray = MCPTool[];
40 | 
```

--------------------------------------------------------------------------------
/.github/workflows/publish-mcp.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Publish to MCP Registry
 2 | 
 3 | on:
 4 |   push:
 5 |     tags: ["v*"]
 6 | 
 7 | jobs:
 8 |   publish:
 9 |     runs-on: ubuntu-latest
10 |     permissions:
11 |       id-token: write # Required for OIDC authentication
12 |       contents: read
13 | 
14 |     steps:
15 |       - name: Checkout code
16 |         uses: actions/checkout@v5
17 | 
18 |       - name: Setup pnpm
19 |         uses: pnpm/action-setup@v3
20 |         with:
21 |           version: 10.12.4
22 | 
23 |       - name: Setup Node.js
24 |         uses: actions/setup-node@v5
25 |         with:
26 |           node-version: "lts/*"
27 | 
28 |       - name: Install dependencies
29 |         run: pnpm install --frozen-lockfile
30 | 
31 |       - name: Run tests
32 |         run: pnpm test --if-present
33 | 
34 |       - name: Build package
35 |         run: pnpm build --if-present
36 | 
37 |       - name: Publish to npm
38 |         run: pnpm publish
39 |         env:
40 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
41 | 
42 |       - name: Install MCP Publisher
43 |         run: |
44 |           curl -L "https://github.com/modelcontextprotocol/registry/releases/download/latest/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
45 | 
46 |       - name: Login to MCP Registry
47 |         run: ./mcp-publisher login github-oidc
48 | 
49 |       - name: Publish to MCP Registry
50 |         run: ./mcp-publisher publish
51 | 
```

--------------------------------------------------------------------------------
/src/tools/url.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import type { Tool, ToolSchema, ToolResult } from "./tool.js";
 3 | import type { Context } from "../context.js";
 4 | import type { ToolActionResult } from "../types/types.js";
 5 | 
 6 | /**
 7 |  * Stagehand Get URL
 8 |  *
 9 |  * This tool is used to get the current URL of the browser page.
10 |  */
11 | 
12 | // Empty schema since getting URL doesn't require any input
13 | const GetUrlInputSchema = z.object({});
14 | 
15 | type GetUrlInput = z.infer<typeof GetUrlInputSchema>;
16 | 
17 | const getUrlSchema: ToolSchema<typeof GetUrlInputSchema> = {
18 |   name: "browserbase_stagehand_get_url",
19 |   description: "Return the current page URL (full URL with query/fragment).",
20 |   inputSchema: GetUrlInputSchema,
21 | };
22 | 
23 | async function handleGetUrl(
24 |   context: Context,
25 |   // eslint-disable-next-line @typescript-eslint/no-unused-vars
26 |   params: GetUrlInput,
27 | ): Promise<ToolResult> {
28 |   const action = async (): Promise<ToolActionResult> => {
29 |     try {
30 |       const stagehand = await context.getStagehand();
31 | 
32 |       // Get the current URL from the Playwright page
33 |       const currentUrl = stagehand.page.url();
34 | 
35 |       return {
36 |         content: [
37 |           {
38 |             type: "text",
39 |             text: currentUrl,
40 |           },
41 |         ],
42 |       };
43 |     } catch (error) {
44 |       const errorMsg = error instanceof Error ? error.message : String(error);
45 |       throw new Error(`Failed to get current URL: ${errorMsg}`);
46 |     }
47 |   };
48 | 
49 |   return {
50 |     action,
51 |     waitForNetwork: false,
52 |   };
53 | }
54 | 
55 | const getUrlTool: Tool<typeof GetUrlInputSchema> = {
56 |   capability: "core",
57 |   schema: getUrlSchema,
58 |   handle: handleGetUrl,
59 | };
60 | 
61 | export default getUrlTool;
62 | 
```

--------------------------------------------------------------------------------
/src/tools/navigate.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import type { Tool, ToolSchema, ToolResult } from "./tool.js";
 3 | import type { Context } from "../context.js";
 4 | import type { ToolActionResult } from "../types/types.js";
 5 | 
 6 | const NavigateInputSchema = z.object({
 7 |   url: z.string().describe("The URL to navigate to"),
 8 | });
 9 | 
10 | type NavigateInput = z.infer<typeof NavigateInputSchema>;
11 | 
12 | const navigateSchema: ToolSchema<typeof NavigateInputSchema> = {
13 |   name: "browserbase_stagehand_navigate",
14 |   description: `Navigate to a URL in the browser. Only use this tool with URLs you're confident will work and be up to date. 
15 |     Otherwise, use https://google.com as the starting point`,
16 |   inputSchema: NavigateInputSchema,
17 | };
18 | 
19 | async function handleNavigate(
20 |   context: Context,
21 |   params: NavigateInput,
22 | ): Promise<ToolResult> {
23 |   const action = async (): Promise<ToolActionResult> => {
24 |     try {
25 |       const stagehand = await context.getStagehand();
26 |       const page = await context.getActivePage();
27 | 
28 |       if (!page) {
29 |         throw new Error("No active page available");
30 |       }
31 |       await page.goto(params.url, { waitUntil: "domcontentloaded" });
32 | 
33 |       const sessionId = stagehand.browserbaseSessionID;
34 |       if (!sessionId) {
35 |         throw new Error("No Browserbase session ID available");
36 |       }
37 | 
38 |       return {
39 |         content: [
40 |           {
41 |             type: "text",
42 |             text: `Navigated to: ${params.url}`,
43 |           },
44 |         ],
45 |       };
46 |     } catch (error) {
47 |       const errorMsg = error instanceof Error ? error.message : String(error);
48 |       throw new Error(`Failed to navigate: ${errorMsg}`);
49 |     }
50 |   };
51 | 
52 |   return {
53 |     action,
54 |     waitForNetwork: false,
55 |   };
56 | }
57 | 
58 | const navigateTool: Tool<typeof NavigateInputSchema> = {
59 |   capability: "core",
60 |   schema: navigateSchema,
61 |   handle: handleNavigate,
62 | };
63 | 
64 | export default navigateTool;
65 | 
```

--------------------------------------------------------------------------------
/evals/mcp-eval-minimal.config.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "passThreshold": 0.7,
 3 |   "server": {
 4 |     "transport": "stdio",
 5 |     "command": "node",
 6 |     "args": ["./cli.js"],
 7 |     "env": {
 8 |       "BROWSERBASE_API_KEY": "${BROWSERBASE_API_KEY}",
 9 |       "BROWSERBASE_PROJECT_ID": "${BROWSERBASE_PROJECT_ID}",
10 |       "GEMINI_API_KEY": "${GEMINI_API_KEY}"
11 |     }
12 |   },
13 |   "timeout": 60000,
14 |   "llmJudge": false,
15 |   "workflows": [
16 |     {
17 |       "name": "smoke-test-navigation",
18 |       "description": "Quick test to verify basic navigation works",
19 |       "steps": [
20 |         {
21 |           "user": "Open a browser and go to example.org",
22 |           "expectedState": "session created"
23 |         },
24 |         {
25 |           "user": "Close the browser",
26 |           "expectedState": "closed successfully via Stagehand"
27 |         }
28 |       ],
29 |       "expectTools": [
30 |         "browserbase_session_create",
31 |         "browserbase_stagehand_navigate",
32 |         "browserbase_session_close"
33 |       ]
34 |     },
35 |     {
36 |       "name": "smoke-test-extraction",
37 |       "description": "Quick test to verify data extraction works",
38 |       "steps": [
39 |         {
40 |           "user": "Navigate to example.org and extract the page title",
41 |           "expectedState": "Example Domain"
42 |         },
43 |         {
44 |           "user": "Close the session",
45 |           "expectedState": "closed successfully via Stagehand"
46 |         }
47 |       ],
48 |       "expectTools": [
49 |         "browserbase_session_create",
50 |         "browserbase_stagehand_navigate",
51 |         "browserbase_stagehand_extract",
52 |         "browserbase_session_close"
53 |       ]
54 |     },
55 |     {
56 |       "name": "smoke-test-url-tools",
57 |       "description": "Quick test to verify URL retrieval tools work",
58 |       "steps": [
59 |         {
60 |           "user": "Create a browser session, navigate to example.org, get the current URL, and close the session",
61 |           "expectedState": "example.org"
62 |         }
63 |       ],
64 |       "expectTools": [
65 |         "browserbase_session_create",
66 |         "browserbase_stagehand_navigate",
67 |         "browserbase_stagehand_get_url",
68 |         "browserbase_session_close"
69 |       ]
70 |     }
71 |   ]
72 | }
73 | 
```

--------------------------------------------------------------------------------
/src/tools/act.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import type { Tool, ToolSchema, ToolResult } from "./tool.js";
 3 | import type { Context } from "../context.js";
 4 | import type { ToolActionResult } from "../types/types.js";
 5 | 
 6 | /**
 7 |  * Stagehand Act
 8 |  * Docs: https://docs.stagehand.dev/basics/act
 9 |  *
10 |  * This tool is used to perform actions on a web page.
11 |  */
12 | 
13 | const ActInputSchema = z.object({
14 |   action: z.string().describe(
15 |     `The action to perform. Should be as atomic and specific as possible,
16 |       i.e. 'Click the sign in button' or 'Type 'hello' into the search input'.`,
17 |   ),
18 |   variables: z
19 |     .object({})
20 |     .optional()
21 |     .describe(
22 |       `Variables used in the action template. ONLY use variables if you're dealing
23 |       with sensitive data or dynamic content. When using variables, you MUST have the variable
24 |       key in the action template. ie: {"action": "Fill in the password", "variables": {"password": "123456"}}`,
25 |     ),
26 | });
27 | 
28 | type ActInput = z.infer<typeof ActInputSchema>;
29 | 
30 | const actSchema: ToolSchema<typeof ActInputSchema> = {
31 |   name: "browserbase_stagehand_act",
32 |   description: `Perform a single action on the page (e.g., click, type).`,
33 |   inputSchema: ActInputSchema,
34 | };
35 | 
36 | async function handleAct(
37 |   context: Context,
38 |   params: ActInput,
39 | ): Promise<ToolResult> {
40 |   const action = async (): Promise<ToolActionResult> => {
41 |     try {
42 |       const stagehand = await context.getStagehand();
43 | 
44 |       await stagehand.page.act({
45 |         action: params.action,
46 |         variables: params.variables,
47 |       });
48 | 
49 |       return {
50 |         content: [
51 |           {
52 |             type: "text",
53 |             text: `Action performed: ${params.action}`,
54 |           },
55 |         ],
56 |       };
57 |     } catch (error) {
58 |       const errorMsg = error instanceof Error ? error.message : String(error);
59 |       throw new Error(`Failed to perform action: ${errorMsg}`);
60 |     }
61 |   };
62 | 
63 |   return {
64 |     action,
65 |     waitForNetwork: false,
66 |   };
67 | }
68 | 
69 | const actTool: Tool<typeof ActInputSchema> = {
70 |   capability: "core",
71 |   schema: actSchema,
72 |   handle: handleAct,
73 | };
74 | 
75 | export default actTool;
76 | 
```

--------------------------------------------------------------------------------
/src/tools/extract.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import type { Tool, ToolSchema, ToolResult } from "./tool.js";
 3 | import type { Context } from "../context.js";
 4 | import type { ToolActionResult } from "../types/types.js";
 5 | 
 6 | /**
 7 |  * Stagehand Extract
 8 |  * Docs: https://docs.stagehand.dev/basics/extract
 9 |  *
10 |  * This tool is used to extract structured information and text content from a web page.
11 |  *
12 |  * We currently don't support the client providing a zod schema for the extraction.
13 |  */
14 | 
15 | const ExtractInputSchema = z.object({
16 |   instruction: z.string().describe(
17 |     `The specific instruction for what information to extract from the current page.
18 |     Be as detailed and specific as possible about what you want to extract. For example:
19 |     'Extract all product names and prices from the listing page'.The more specific your instruction,
20 |     the better the extraction results will be.`,
21 |   ),
22 | });
23 | 
24 | type ExtractInput = z.infer<typeof ExtractInputSchema>;
25 | 
26 | const extractSchema: ToolSchema<typeof ExtractInputSchema> = {
27 |   name: "browserbase_stagehand_extract",
28 |   description: `Extract structured data or text from the current page using an instruction.`,
29 |   inputSchema: ExtractInputSchema,
30 | };
31 | 
32 | async function handleExtract(
33 |   context: Context,
34 |   params: ExtractInput,
35 | ): Promise<ToolResult> {
36 |   const action = async (): Promise<ToolActionResult> => {
37 |     try {
38 |       const stagehand = await context.getStagehand();
39 |       const extraction = await stagehand.page.extract(params.instruction);
40 | 
41 |       return {
42 |         content: [
43 |           {
44 |             type: "text",
45 |             text: `Extracted content:\n${JSON.stringify(extraction, null, 2)}`,
46 |           },
47 |         ],
48 |       };
49 |     } catch (error) {
50 |       const errorMsg = error instanceof Error ? error.message : String(error);
51 |       throw new Error(`Failed to extract content: ${errorMsg}`);
52 |     }
53 |   };
54 | 
55 |   return {
56 |     action,
57 |     waitForNetwork: false,
58 |   };
59 | }
60 | 
61 | const extractTool: Tool<typeof ExtractInputSchema> = {
62 |   capability: "core",
63 |   schema: extractSchema,
64 |   handle: handleExtract,
65 | };
66 | 
67 | export default extractTool;
68 | 
```

--------------------------------------------------------------------------------
/server.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
 3 |   "name": "io.github.browserbase/mcp-server-browserbase",
 4 |   "description": "MCP server for AI web browser automation using Browserbase and Stagehand",
 5 |   "status": "active",
 6 |   "repository": {
 7 |     "url": "https://github.com/browserbase/mcp-server-browserbase",
 8 |     "source": "github"
 9 |   },
10 |   "version": "2.2.0",
11 |   "packages": [
12 |     {
13 |       "registry_type": "npm",
14 |       "registry_base_url": "https://registry.npmjs.org",
15 |       "identifier": "@browserbasehq/mcp-server-browserbase",
16 |       "version": "2.2.0",
17 |       "transport": {
18 |         "type": "stdio"
19 |       },
20 |       "environment_variables": [
21 |         {
22 |           "description": "Your Browserbase API key",
23 |           "is_required": true,
24 |           "format": "string",
25 |           "is_secret": true,
26 |           "name": "BROWSERBASE_API_KEY"
27 |         },
28 |         {
29 |           "description": "Your Browserbase Project ID",
30 |           "is_required": true,
31 |           "format": "string",
32 |           "is_secret": false,
33 |           "name": "BROWSERBASE_PROJECT_ID"
34 |         },
35 |         {
36 |           "description": "Your Gemini API key (default model)",
37 |           "is_required": true,
38 |           "format": "string",
39 |           "is_secret": true,
40 |           "name": "GEMINI_API_KEY"
41 |         }
42 |       ]
43 |     },
44 |     {
45 |       "registry_type": "oci",
46 |       "identifier": "browserbasehq/mcp-server-browserbase",
47 |       "version": "2.2.0",
48 |       "runtime_hint": "docker",
49 |       "environment_variables": [
50 |         {
51 |           "description": "Your Browserbase API key",
52 |           "is_required": true,
53 |           "format": "string",
54 |           "is_secret": true,
55 |           "name": "BROWSERBASE_API_KEY"
56 |         },
57 |         {
58 |           "description": "Your Browserbase Project ID",
59 |           "is_required": true,
60 |           "format": "string",
61 |           "is_secret": false,
62 |           "name": "BROWSERBASE_PROJECT_ID"
63 |         },
64 |         {
65 |           "description": "Your Gemini API key (default model)",
66 |           "is_required": true,
67 |           "format": "string",
68 |           "is_secret": true,
69 |           "name": "GEMINI_API_KEY"
70 |         }
71 |       ],
72 |       "transport": {
73 |         "type": "stdio"
74 |       }
75 |     }
76 |   ]
77 | }
78 | 
```

--------------------------------------------------------------------------------
/src/mcp/resources.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Resources module for the Browserbase MCP server
 3 |  * Contains resources definitions and handlers for resource-related requests
 4 |  * Docs: https://modelcontextprotocol.io/docs/concepts/resources
 5 |  */
 6 | 
 7 | // Define the resources
 8 | export const RESOURCES = [];
 9 | 
10 | // Define the resource templates
11 | export const RESOURCE_TEMPLATES = [];
12 | 
13 | // Store screenshots in a map
14 | export const screenshots = new Map<string, string>();
15 | 
16 | // Track screenshots by session so we can purge them on session end
17 | // key: sessionId (internal/current session id), value: set of screenshot names
18 | const sessionIdToScreenshotNames = new Map<string, Set<string>>();
19 | 
20 | export function registerScreenshot(
21 |   sessionId: string,
22 |   name: string,
23 |   base64: string,
24 | ) {
25 |   screenshots.set(name, base64);
26 |   let set = sessionIdToScreenshotNames.get(sessionId);
27 |   if (!set) {
28 |     set = new Set();
29 |     sessionIdToScreenshotNames.set(sessionId, set);
30 |   }
31 |   set.add(name);
32 | }
33 | 
34 | export function clearScreenshotsForSession(sessionId: string) {
35 |   const set = sessionIdToScreenshotNames.get(sessionId);
36 |   if (set) {
37 |     for (const name of set) {
38 |       screenshots.delete(name);
39 |     }
40 |     sessionIdToScreenshotNames.delete(sessionId);
41 |   }
42 | }
43 | 
44 | export function clearAllScreenshots() {
45 |   screenshots.clear();
46 |   sessionIdToScreenshotNames.clear();
47 | }
48 | 
49 | /**
50 |  * Handle listing resources request
51 |  * @returns A list of available resources including screenshots
52 |  */
53 | export function listResources() {
54 |   return {
55 |     resources: [
56 |       ...Array.from(screenshots.keys()).map((name) => ({
57 |         uri: `screenshot://${name}`,
58 |         mimeType: "image/png",
59 |         name: `Screenshot: ${name}`,
60 |       })),
61 |     ],
62 |   };
63 | }
64 | 
65 | /**
66 |  * Handle listing resource templates request
67 |  * @returns An empty resource templates list response
68 |  */
69 | export function listResourceTemplates() {
70 |   return { resourceTemplates: [] };
71 | }
72 | 
73 | /**
74 |  * Read a resource by its URI
75 |  * @param uri The URI of the resource to read
76 |  * @returns The resource content or throws if not found
77 |  */
78 | export function readResource(uri: string) {
79 |   if (uri.startsWith("screenshot://")) {
80 |     const name = uri.split("://")[1];
81 |     const screenshot = screenshots.get(name);
82 |     if (screenshot) {
83 |       return {
84 |         contents: [
85 |           {
86 |             uri,
87 |             mimeType: "image/png",
88 |             blob: screenshot,
89 |           },
90 |         ],
91 |       };
92 |     }
93 |   }
94 | 
95 |   throw new Error(`Resource not found: ${uri}`);
96 | }
97 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@browserbasehq/mcp-server-browserbase",
 3 |   "version": "2.2.0",
 4 |   "description": "MCP server for AI web browser automation using Browserbase and Stagehand",
 5 |   "mcpName": "io.github.browserbase/mcp-server-browserbase",
 6 |   "license": "Apache-2.0",
 7 |   "author": "Browserbase, Inc. (https://www.browserbase.com/)",
 8 |   "homepage": "https://www.browserbase.com",
 9 |   "bugs": "https://github.com/modelcontextprotocol/servers/issues",
10 |   "type": "module",
11 |   "main": "./cli.js",
12 |   "module": "./src/index.ts",
13 |   "bin": {
14 |     "mcp-server-browserbase": "cli.js"
15 |   },
16 |   "files": [
17 |     "assets",
18 |     "README.md",
19 |     "dist",
20 |     "cli.js",
21 |     "index.d.ts",
22 |     "index.js",
23 |     "config.d.ts"
24 |   ],
25 |   "scripts": {
26 |     "build": "tsc && shx chmod +x dist/*.js",
27 |     "prepare": "husky && pnpm build",
28 |     "watch": "tsc --watch",
29 |     "smithery": "npx @smithery/cli dev src/index.ts",
30 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js",
31 |     "evals": "tsx evals/run-evals.ts run --config evals/mcp-eval-basic.config.json && tsx evals/run-evals.ts run --config evals/mcp-eval-minimal.config.json && tsx evals/run-evals.ts run --config evals/mcp-eval.config.json",
32 |     "lint": "eslint . --ext .ts",
33 |     "format": "prettier --write .",
34 |     "clean": "rm -rf dist",
35 |     "prepublishOnly": "pnpm clean && pnpm build",
36 |     "pre-commit": "pnpm lint-staged",
37 |     "changeset": "changeset",
38 |     "version:packages": "changeset version",
39 |     "release": "changeset publish"
40 |   },
41 |   "lint-staged": {
42 |     "*.{js,jsx,ts,tsx,json,css,scss,md}": [
43 |       "prettier --write .",
44 |       "eslint --fix"
45 |     ]
46 |   },
47 |   "dependencies": {
48 |     "@browserbasehq/sdk": "^2.6.0",
49 |     "@browserbasehq/stagehand": "^2.5.2",
50 |     "@mcp-ui/server": "^5.10.0",
51 |     "@modelcontextprotocol/sdk": "^1.13.1",
52 |     "commander": "^14.0.0",
53 |     "dotenv": "^16.4.6",
54 |     "mcpvals": "^0.0.3",
55 |     "zod": "^3.25.67"
56 |   },
57 |   "devDependencies": {
58 |     "@changesets/cli": "^2.29.6",
59 |     "@eslint/js": "^9.29.0",
60 |     "@smithery/cli": "^1.2.15",
61 |     "chalk": "^5.3.0",
62 |     "eslint": "^9.29.0",
63 |     "eslint-plugin-react": "^7.37.5",
64 |     "globals": "^16.2.0",
65 |     "husky": "^9.1.7",
66 |     "lint-staged": "^16.1.2",
67 |     "playwright-core": "^1.53.2",
68 |     "prettier": "^3.6.1",
69 |     "shx": "^0.3.4",
70 |     "tsx": "^4.20.3",
71 |     "typescript": "^5.6.2",
72 |     "typescript-eslint": "^8.35.0"
73 |   },
74 |   "publishConfig": {
75 |     "access": "public"
76 |   },
77 |   "packageManager": "[email protected]+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
78 | }
79 | 
```

--------------------------------------------------------------------------------
/src/tools/screenshot.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import type { Tool, ToolSchema, ToolResult } from "./tool.js";
 3 | import type { Context } from "../context.js";
 4 | import type { ToolActionResult } from "../types/types.js";
 5 | import { registerScreenshot } from "../mcp/resources.js";
 6 | 
 7 | /**
 8 |  * Screenshot
 9 |  * Docs: https://playwright.dev/docs/screenshots
10 |  *
11 |  * This tool is used to take a screenshot of the current page.
12 |  */
13 | 
14 | const ScreenshotInputSchema = z.object({
15 |   name: z.string().optional().describe("The name of the screenshot"),
16 | });
17 | 
18 | type ScreenshotInput = z.infer<typeof ScreenshotInputSchema>;
19 | 
20 | const screenshotSchema: ToolSchema<typeof ScreenshotInputSchema> = {
21 |   name: "browserbase_screenshot",
22 |   description: `Capture a full-page screenshot and return it (and save as a resource).`,
23 |   inputSchema: ScreenshotInputSchema,
24 | };
25 | 
26 | async function handleScreenshot(
27 |   context: Context,
28 |   params: ScreenshotInput,
29 | ): Promise<ToolResult> {
30 |   const action = async (): Promise<ToolActionResult> => {
31 |     try {
32 |       const page = await context.getActivePage();
33 |       if (!page) {
34 |         throw new Error("No active page available");
35 |       }
36 | 
37 |       // We're taking a full page screenshot to give context of the entire page, similar to a snapshot
38 |       const screenshotBuffer = await page.screenshot({
39 |         fullPage: true,
40 |       });
41 | 
42 |       // Convert buffer to base64 string and store in memory
43 |       const screenshotBase64 = screenshotBuffer.toString("base64");
44 |       const name = params.name
45 |         ? `screenshot-${params.name}-${new Date()
46 |             .toISOString()
47 |             .replace(/:/g, "-")}`
48 |         : `screenshot-${new Date().toISOString().replace(/:/g, "-")}` +
49 |           context.config.browserbaseProjectId;
50 | 
51 |       // Associate with current mcp session id and store in memory /src/mcp/resources.ts
52 |       const sessionId = context.currentSessionId;
53 |       registerScreenshot(sessionId, name, screenshotBase64);
54 | 
55 |       // Notify the client that the resources changed
56 |       const serverInstance = context.getServer();
57 | 
58 |       if (serverInstance) {
59 |         serverInstance.notification({
60 |           method: "notifications/resources/list_changed",
61 |         });
62 |       }
63 | 
64 |       return {
65 |         content: [
66 |           {
67 |             type: "text",
68 |             text: `Screenshot taken with name: ${name}`,
69 |           },
70 |           {
71 |             type: "image",
72 |             data: screenshotBase64,
73 |             mimeType: "image/png",
74 |           },
75 |         ],
76 |       };
77 |     } catch (error) {
78 |       const errorMsg = error instanceof Error ? error.message : String(error);
79 |       throw new Error(`Failed to take screenshot: ${errorMsg}`);
80 |     }
81 |   };
82 | 
83 |   return {
84 |     action,
85 |     waitForNetwork: false,
86 |   };
87 | }
88 | 
89 | const screenshotTool: Tool<typeof ScreenshotInputSchema> = {
90 |   capability: "core",
91 |   schema: screenshotSchema,
92 |   handle: handleScreenshot,
93 | };
94 | 
95 | export default screenshotTool;
96 | 
```

--------------------------------------------------------------------------------
/src/tools/observe.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import type { Tool, ToolSchema, ToolResult } from "./tool.js";
 3 | import type { Context } from "../context.js";
 4 | import type { ToolActionResult } from "../types/types.js";
 5 | 
 6 | /**
 7 |  * Stagehand Observe
 8 |  * Docs: https://docs.stagehand.dev/basics/observe
 9 |  *
10 |  * This tool is used to observe and identify specific interactive elements on a web page.
11 |  * You can optionally choose to have the observe tool return an action to perform on the element.
12 |  */
13 | 
14 | const ObserveInputSchema = z.object({
15 |   instruction: z.string().describe(
16 |     `Detailed instruction for what specific elements or components to observe on the web page.
17 |         This instruction must be extremely specific and descriptive. For example: 'Find the red login button
18 |         in the top right corner', 'Locate the search input field with placeholder text', or 'Identify all
19 |         clickable product cards on the page'. The more specific and detailed your instruction, the better
20 |         the observation results will be. Avoid generic instructions like 'find buttons' or 'see elements'.
21 |         Instead, describe the visual characteristics, location, text content, or functionality of the elements
22 |         you want to observe. This tool is designed to help you identify interactive elements that you can
23 |         later use with the act tool for performing actions like clicking, typing, or form submission.`,
24 |   ),
25 |   returnAction: z
26 |     .boolean()
27 |     .optional()
28 |     .describe(
29 |       `Whether to return the action to perform on the element. If true, the action will be returned as a string.
30 |        If false, the action will not be returned.`,
31 |     ),
32 | });
33 | 
34 | type ObserveInput = z.infer<typeof ObserveInputSchema>;
35 | 
36 | const observeSchema: ToolSchema<typeof ObserveInputSchema> = {
37 |   name: "browserbase_stagehand_observe",
38 |   description: `Find interactive elements on the page from an instruction; optionally return an action.`,
39 |   inputSchema: ObserveInputSchema,
40 | };
41 | 
42 | async function handleObserve(
43 |   context: Context,
44 |   params: ObserveInput,
45 | ): Promise<ToolResult> {
46 |   const action = async (): Promise<ToolActionResult> => {
47 |     try {
48 |       const stagehand = await context.getStagehand();
49 | 
50 |       const observations = await stagehand.page.observe({
51 |         instruction: params.instruction,
52 |         returnAction: params.returnAction,
53 |       });
54 | 
55 |       return {
56 |         content: [
57 |           {
58 |             type: "text",
59 |             text: `Observations: ${JSON.stringify(observations)}`,
60 |           },
61 |         ],
62 |       };
63 |     } catch (error) {
64 |       const errorMsg = error instanceof Error ? error.message : String(error);
65 |       throw new Error(`Failed to observe: ${errorMsg}`);
66 |     }
67 |   };
68 | 
69 |   return {
70 |     action,
71 |     waitForNetwork: false,
72 |   };
73 | }
74 | 
75 | const observeTool: Tool<typeof ObserveInputSchema> = {
76 |   capability: "core",
77 |   schema: observeSchema,
78 |   handle: handleObserve,
79 | };
80 | 
81 | export default observeTool;
82 | 
```

--------------------------------------------------------------------------------
/config.d.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { Cookie } from "playwright-core";
  2 | import type { AvailableModelSchema } from "@browserbasehq/stagehand";
  3 | 
  4 | export type Config = {
  5 |   /**
  6 |    * Browserbase API Key to authenticate requests
  7 |    */
  8 |   browserbaseApiKey: string;
  9 |   /**
 10 |    * Browserbase Project ID associated with the API key
 11 |    */
 12 |   browserbaseProjectId: string;
 13 |   /**
 14 |    * Whether or not to use Browserbase proxies
 15 |    * https://docs.browserbase.com/features/proxies
 16 |    *
 17 |    * @default false
 18 |    */
 19 |   proxies?: boolean;
 20 |   /**
 21 |    * Use advanced stealth mode. Only available to Browserbase Scale Plan users.
 22 |    *
 23 |    * @default false
 24 |    */
 25 |   advancedStealth?: boolean;
 26 |   /**
 27 |    * Whether or not to keep the Browserbase session alive
 28 |    *
 29 |    * @default false
 30 |    */
 31 |   keepAlive?: boolean;
 32 |   /**
 33 |    * Potential Browserbase Context to use
 34 |    * Would be a context ID
 35 |    */
 36 |   context?: {
 37 |     /**
 38 |      * The ID of the context to use
 39 |      */
 40 |     contextId?: string;
 41 |     /**
 42 |      * Whether or not to persist the context
 43 |      *
 44 |      * @default true
 45 |      */
 46 |     persist?: boolean;
 47 |   };
 48 |   /**
 49 |    * The viewport of the browser
 50 |    * @default { browserWidth: 1024, browserHeight: 768 }
 51 |    */
 52 |   viewPort?: {
 53 |     /**
 54 |      * The width of the browser
 55 |      */
 56 |     browserWidth?: number;
 57 |     /**
 58 |      * The height of the browser
 59 |      */
 60 |     browserHeight?: number;
 61 |   };
 62 |   /**
 63 |    * Cookies to inject into the Browserbase context
 64 |    * Format: Array of cookie objects with name, value, domain, and optional path, expires, httpOnly, secure, sameSite
 65 |    */
 66 |   cookies?: Cookie[];
 67 |   /**
 68 |    * Server configuration for MCP transport layer
 69 |    *
 70 |    * Controls how the MCP server binds and listens for connections.
 71 |    * When port is specified, the server will start an SHTTP transport.
 72 |    * When both port and host are undefined, the server uses stdio transport.
 73 |    *
 74 |    * Security considerations:
 75 |    * - Use localhost (default) for local development
 76 |    * - Use 0.0.0.0 only when you need external access and have proper security measures
 77 |    * - Consider firewall rules and network security when exposing the server
 78 |    */
 79 |   server?: {
 80 |     /**
 81 |      * The port to listen on for SHTTP or MCP transport.
 82 |      * If undefined, uses stdio transport instead of HTTP.
 83 |      *
 84 |      * @example 3000
 85 |      */
 86 |     port?: number;
 87 |     /**
 88 |      * The host to bind the server to.
 89 |      *
 90 |      * @default "localhost" - Only accepts local connections
 91 |      * @example "0.0.0.0" - Accepts connections from any interface (use with caution)
 92 |      */
 93 |     host?: string;
 94 |   };
 95 |   /**
 96 |    * The Model that Stagehand uses
 97 |    * Available models: OpenAI, Claude, Gemini, Cerebras, Groq, and other providers
 98 |    *
 99 |    * @default "gemini-2.0-flash"
100 |    */
101 |   modelName?: z.infer<typeof AvailableModelSchema>;
102 |   /**
103 |    * API key for the custom model provider
104 |    * Required when using a model other than the default gemini-2.0-flash
105 |    */
106 |   modelApiKey?: string;
107 |   /**
108 |    * Enable experimental features
109 |    *
110 |    * @default false
111 |    */
112 |   experimental?: boolean;
113 | };
114 | 
```

--------------------------------------------------------------------------------
/src/program.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { program } from "commander";
  2 | import * as fs from "fs";
  3 | import * as path from "path";
  4 | import { fileURLToPath } from "url";
  5 | 
  6 | import createServerFunction from "./index.js";
  7 | import { ServerList } from "./server.js";
  8 | import { startHttpTransport, startStdioTransport } from "./transport.js";
  9 | 
 10 | import { resolveConfig } from "./config.js";
 11 | 
 12 | let __filename: string;
 13 | let __dirname: string;
 14 | 
 15 | try {
 16 |   // Try ES modules first
 17 |   __filename = fileURLToPath(import.meta.url);
 18 |   __dirname = path.dirname(__filename);
 19 | } catch {
 20 |   // Fallback for CommonJS or when import.meta is not available
 21 |   __filename =
 22 |     (globalThis as { __filename: string }).__filename ||
 23 |     process.cwd() + "/dist/program.js";
 24 |   __dirname = path.dirname(__filename);
 25 | }
 26 | 
 27 | // Load package.json using fs
 28 | const packageJSONPath = path.resolve(__dirname, "../package.json");
 29 | const packageJSONBuffer = fs.readFileSync(packageJSONPath);
 30 | const packageJSON = JSON.parse(packageJSONBuffer.toString());
 31 | 
 32 | program
 33 |   .version("Version " + packageJSON.version)
 34 |   .name(packageJSON.name)
 35 |   .option("--browserbaseApiKey <key>", "The Browserbase API Key to use")
 36 |   .option("--browserbaseProjectId <id>", "The Browserbase Project ID to use")
 37 |   .option("--proxies", "Use Browserbase proxies.")
 38 |   .option(
 39 |     "--advancedStealth",
 40 |     "Use advanced stealth mode. Only available to Browserbase Scale Plan users.",
 41 |   )
 42 |   .option("--contextId <contextId>", "Browserbase Context ID to use.")
 43 |   .option(
 44 |     "--persist [boolean]",
 45 |     "Whether to persist the Browserbase context",
 46 |     true,
 47 |   )
 48 |   .option("--port <port>", "Port to listen on for SHTTP transport.")
 49 |   .option(
 50 |     "--host <host>",
 51 |     "Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.",
 52 |   )
 53 |   .option(
 54 |     "--cookies [json]",
 55 |     'JSON array of cookies to inject into the browser. Format: [{"name":"cookie1","value":"val1","domain":"example.com"}, ...]',
 56 |   )
 57 |   .option("--browserWidth <width>", "Browser width to use for the browser.")
 58 |   .option("--browserHeight <height>", "Browser height to use for the browser.")
 59 |   .option(
 60 |     "--modelName <model>",
 61 |     "The model to use for Stagehand (default: gemini-2.0-flash)",
 62 |   )
 63 |   .option(
 64 |     "--modelApiKey <key>",
 65 |     "API key for the custom model provider (required when using custom models)",
 66 |   )
 67 |   .option("--keepAlive", "Enable Browserbase Keep Alive Session")
 68 |   .option("--experimental", "Enable experimental features")
 69 |   .action(async (options) => {
 70 |     const config = await resolveConfig(options);
 71 |     const serverList = new ServerList(async () =>
 72 |       createServerFunction({
 73 |         config: config,
 74 |       }),
 75 |     );
 76 |     setupExitWatchdog(serverList);
 77 | 
 78 |     if (options.port)
 79 |       startHttpTransport(+options.port, options.host, serverList);
 80 |     else await startStdioTransport(serverList, config);
 81 |   });
 82 | 
 83 | function setupExitWatchdog(serverList: ServerList) {
 84 |   const handleExit = async () => {
 85 |     setTimeout(() => process.exit(0), 15000);
 86 |     try {
 87 |       // SessionManager within each server handles session cleanup
 88 |       await serverList.closeAll();
 89 |     } catch (error) {
 90 |       console.error("Error during cleanup:", error);
 91 |     }
 92 |     process.exit(0);
 93 |   };
 94 | 
 95 |   process.stdin.on("close", handleExit);
 96 |   process.on("SIGINT", handleExit);
 97 |   process.on("SIGTERM", handleExit);
 98 | }
 99 | 
100 | program.parse(process.argv);
101 | 
```

--------------------------------------------------------------------------------
/evals/mcp-eval.config.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "passThreshold": 0.7,
  3 |   "server": {
  4 |     "transport": "stdio",
  5 |     "command": "node",
  6 |     "args": ["./cli.js"],
  7 |     "env": {
  8 |       "BROWSERBASE_API_KEY": "${BROWSERBASE_API_KEY}",
  9 |       "BROWSERBASE_PROJECT_ID": "${BROWSERBASE_PROJECT_ID}",
 10 |       "GEMINI_API_KEY": "${GEMINI_API_KEY}"
 11 |     }
 12 |   },
 13 |   "timeout": 180000,
 14 |   "llmJudge": false,
 15 |   "workflows": [
 16 |     {
 17 |       "name": "basic-navigation-test",
 18 |       "description": "Test basic browser navigation functionality",
 19 |       "steps": [
 20 |         {
 21 |           "user": "Create a browser session, navigate to https://example.com, and close the session",
 22 |           "expectedState": "closed"
 23 |         }
 24 |       ],
 25 |       "expectTools": [
 26 |         "browserbase_session_create",
 27 |         "browserbase_stagehand_navigate",
 28 |         "browserbase_session_close"
 29 |       ]
 30 |     },
 31 |     {
 32 |       "name": "search-and-extract-test",
 33 |       "description": "Test navigation, search interaction, and data extraction",
 34 |       "steps": [
 35 |         {
 36 |           "user": "Create a browser session, navigate to https://example.com, extract the page title, and close the session",
 37 |           "expectedState": "Example Domain"
 38 |         }
 39 |       ],
 40 |       "expectTools": [
 41 |         "browserbase_session_create",
 42 |         "browserbase_stagehand_navigate",
 43 |         "browserbase_stagehand_extract",
 44 |         "browserbase_session_close"
 45 |       ]
 46 |     },
 47 |     {
 48 |       "name": "observe-and-interact-test",
 49 |       "description": "Test element observation and interaction capabilities",
 50 |       "steps": [
 51 |         {
 52 |           "user": "Create a browser session, navigate to https://example.com, observe the page elements, and close the session",
 53 |           "expectedState": "closed"
 54 |         }
 55 |       ],
 56 |       "expectTools": [
 57 |         "browserbase_session_create",
 58 |         "browserbase_stagehand_navigate",
 59 |         "browserbase_stagehand_observe",
 60 |         "browserbase_session_close"
 61 |       ]
 62 |     },
 63 |     {
 64 |       "name": "screenshot-test",
 65 |       "description": "Test screenshot functionality",
 66 |       "steps": [
 67 |         {
 68 |           "user": "Create a browser session, navigate to https://example.com, take a screenshot, and close the session",
 69 |           "expectedState": "closed"
 70 |         }
 71 |       ],
 72 |       "expectTools": [
 73 |         "browserbase_session_create",
 74 |         "browserbase_stagehand_navigate",
 75 |         "browserbase_screenshot",
 76 |         "browserbase_session_close"
 77 |       ]
 78 |     },
 79 |     {
 80 |       "name": "form-interaction-test",
 81 |       "description": "Test form filling and submission capabilities",
 82 |       "steps": [
 83 |         {
 84 |           "user": "Create a browser session, navigate to https://httpbin.org/forms/post, fill in the customer name field with 'TestUser', and close the session",
 85 |           "expectedState": "closed"
 86 |         }
 87 |       ],
 88 |       "expectTools": [
 89 |         "browserbase_session_create",
 90 |         "browserbase_stagehand_navigate",
 91 |         "browserbase_stagehand_act",
 92 |         "browserbase_session_close"
 93 |       ]
 94 |     },
 95 |     {
 96 |       "name": "error-handling-test",
 97 |       "description": "Test error handling for invalid operations",
 98 |       "steps": [
 99 |         {
100 |           "user": "Create a browser session and try to navigate to an invalid URL like 'invalid-url-test'",
101 |           "expectedState": "error"
102 |         }
103 |       ],
104 |       "expectTools": [
105 |         "browserbase_session_create",
106 |         "browserbase_stagehand_navigate"
107 |       ]
108 |     }
109 |   ]
110 | }
111 | 
```

--------------------------------------------------------------------------------
/src/transport.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import http from "node:http";
  2 | import assert from "node:assert";
  3 | import crypto from "node:crypto";
  4 | 
  5 | import { ServerList } from "./server.js";
  6 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  7 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
  8 | import type { Config } from "../config.d.ts";
  9 | 
 10 | export async function startStdioTransport(
 11 |   serverList: ServerList,
 12 |   config?: Config,
 13 | ) {
 14 |   // Check if we're using the default model without an API key
 15 |   if (config) {
 16 |     const modelName = config.modelName || "gemini-2.0-flash";
 17 |     const hasModelApiKey =
 18 |       config.modelApiKey ||
 19 |       process.env.GEMINI_API_KEY ||
 20 |       process.env.GOOGLE_API_KEY;
 21 | 
 22 |     if (modelName.includes("gemini") && !hasModelApiKey) {
 23 |       console.error(
 24 |         `Need to set GEMINI_API_KEY or GOOGLE_API_KEY in your environment variables`,
 25 |       );
 26 |     }
 27 |   }
 28 | 
 29 |   const server = await serverList.create();
 30 |   await server.connect(new StdioServerTransport());
 31 | }
 32 | 
 33 | async function handleStreamable(
 34 |   req: http.IncomingMessage,
 35 |   res: http.ServerResponse,
 36 |   serverList: ServerList,
 37 |   sessions: Map<string, StreamableHTTPServerTransport>,
 38 | ) {
 39 |   const sessionId = req.headers["mcp-session-id"] as string | undefined;
 40 |   if (sessionId) {
 41 |     const transport = sessions.get(sessionId);
 42 |     if (!transport) {
 43 |       res.statusCode = 404;
 44 |       res.end("Session not found");
 45 |       return;
 46 |     }
 47 |     return await transport.handleRequest(req, res);
 48 |   }
 49 | 
 50 |   if (req.method === "POST") {
 51 |     const sessionId = crypto.randomUUID();
 52 |     const transport = new StreamableHTTPServerTransport({
 53 |       sessionIdGenerator: () => sessionId,
 54 |     });
 55 |     sessions.set(sessionId, transport);
 56 |     transport.onclose = () => {
 57 |       if (transport.sessionId) sessions.delete(transport.sessionId);
 58 |     };
 59 |     const server = await serverList.create();
 60 |     await server.connect(transport);
 61 |     return await transport.handleRequest(req, res);
 62 |   }
 63 | 
 64 |   res.statusCode = 400;
 65 |   res.end("Invalid request");
 66 | }
 67 | 
 68 | export function startHttpTransport(
 69 |   port: number,
 70 |   hostname: string | undefined,
 71 |   serverList: ServerList,
 72 | ) {
 73 |   // In-memory Map of SHTTP sessions
 74 |   const streamableSessions = new Map<string, StreamableHTTPServerTransport>();
 75 |   const httpServer = http.createServer(async (req, res) => {
 76 |     if (!req.url) {
 77 |       res.statusCode = 400;
 78 |       res.end("Bad request: missing URL");
 79 |       return;
 80 |     }
 81 |     const url = new URL(`http://localhost${req.url}`);
 82 |     if (url.pathname.startsWith("/mcp"))
 83 |       await handleStreamable(req, res, serverList, streamableSessions);
 84 |   });
 85 |   httpServer.listen(port, hostname, () => {
 86 |     const address = httpServer.address();
 87 |     assert(address, "Could not bind server socket");
 88 |     let url: string;
 89 |     if (typeof address === "string") {
 90 |       url = address;
 91 |     } else {
 92 |       const resolvedPort = address.port;
 93 |       let resolvedHost =
 94 |         address.family === "IPv4" ? address.address : `[${address.address}]`;
 95 |       if (resolvedHost === "0.0.0.0" || resolvedHost === "[::]")
 96 |         resolvedHost = "localhost";
 97 |       url = `http://${resolvedHost}:${resolvedPort}`;
 98 |     }
 99 |     const message = [
100 |       `Listening on ${url}`,
101 |       "Put this in your client config:",
102 |       JSON.stringify(
103 |         {
104 |           mcpServers: {
105 |             browserbase: {
106 |               url: `${url}/mcp`,
107 |             },
108 |           },
109 |         },
110 |         undefined,
111 |         2,
112 |       ),
113 |       "If your client supports streamable HTTP, you can use the /mcp endpoint instead.",
114 |     ].join("\n");
115 |     console.log(message);
116 |   });
117 | }
118 | 
```

--------------------------------------------------------------------------------
/evals/mcp-eval-basic.config.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "passThreshold": 0.7,
  3 |   "server": {
  4 |     "transport": "stdio",
  5 |     "command": "node",
  6 |     "args": ["./cli.js"],
  7 |     "env": {
  8 |       "BROWSERBASE_API_KEY": "${BROWSERBASE_API_KEY}",
  9 |       "BROWSERBASE_PROJECT_ID": "${BROWSERBASE_PROJECT_ID}",
 10 |       "GEMINI_API_KEY": "${GEMINI_API_KEY}"
 11 |     }
 12 |   },
 13 |   "timeout": 180000,
 14 |   "llmJudge": false,
 15 |   "workflows": [
 16 |     {
 17 |       "name": "basic-navigation-test",
 18 |       "description": "Test basic browser navigation functionality",
 19 |       "steps": [
 20 |         {
 21 |           "user": "Create a browser session, navigate to https://example.com, and close the session",
 22 |           "expectedState": "closed"
 23 |         }
 24 |       ],
 25 |       "expectTools": [
 26 |         "browserbase_session_create",
 27 |         "browserbase_stagehand_navigate",
 28 |         "browserbase_session_close"
 29 |       ]
 30 |     },
 31 |     {
 32 |       "name": "search-and-extract-test",
 33 |       "description": "Test navigation, search interaction, and data extraction",
 34 |       "steps": [
 35 |         {
 36 |           "user": "Create a browser session, navigate to https://example.com, extract the page title, and close the session",
 37 |           "expectedState": "Example Domain"
 38 |         }
 39 |       ],
 40 |       "expectTools": [
 41 |         "browserbase_session_create",
 42 |         "browserbase_stagehand_navigate",
 43 |         "browserbase_stagehand_extract",
 44 |         "browserbase_session_close"
 45 |       ]
 46 |     },
 47 |     {
 48 |       "name": "observe-and-interact-test",
 49 |       "description": "Test element observation and interaction capabilities",
 50 |       "steps": [
 51 |         {
 52 |           "user": "Create a browser session, navigate to https://example.com, observe the page elements, and close the session",
 53 |           "expectedState": "closed"
 54 |         }
 55 |       ],
 56 |       "expectTools": [
 57 |         "browserbase_session_create",
 58 |         "browserbase_stagehand_navigate",
 59 |         "browserbase_stagehand_observe",
 60 |         "browserbase_session_close"
 61 |       ]
 62 |     },
 63 |     {
 64 |       "name": "screenshot-test",
 65 |       "description": "Test screenshot functionality",
 66 |       "steps": [
 67 |         {
 68 |           "user": "Create a browser session, navigate to https://example.com, take a screenshot, and close the session",
 69 |           "expectedState": "closed"
 70 |         }
 71 |       ],
 72 |       "expectTools": [
 73 |         "browserbase_session_create",
 74 |         "browserbase_stagehand_navigate",
 75 |         "browserbase_screenshot",
 76 |         "browserbase_session_close"
 77 |       ]
 78 |     },
 79 |     {
 80 |       "name": "form-interaction-test",
 81 |       "description": "Test form filling and submission capabilities",
 82 |       "steps": [
 83 |         {
 84 |           "user": "Create a browser session, navigate to https://httpbin.org/forms/post, fill in the customer name field with 'TestUser', and close the session",
 85 |           "expectedState": "closed"
 86 |         }
 87 |       ],
 88 |       "expectTools": [
 89 |         "browserbase_session_create",
 90 |         "browserbase_stagehand_navigate",
 91 |         "browserbase_stagehand_act",
 92 |         "browserbase_session_close"
 93 |       ]
 94 |     },
 95 |     {
 96 |       "name": "error-handling-test",
 97 |       "description": "Test error handling for invalid operations",
 98 |       "steps": [
 99 |         {
100 |           "user": "Create a browser session and try to navigate to an invalid URL like 'invalid-url-test'",
101 |           "expectedState": "error"
102 |         }
103 |       ],
104 |       "expectTools": [
105 |         "browserbase_session_create",
106 |         "browserbase_stagehand_navigate"
107 |       ]
108 |     },
109 |     {
110 |       "name": "url-retrieval-test",
111 |       "description": "Test URL retrieval functionality",
112 |       "steps": [
113 |         {
114 |           "user": "Create a browser session, navigate to https://example.com, get the current URL to verify navigation, and close the session",
115 |           "expectedState": "https://example.com"
116 |         }
117 |       ],
118 |       "expectTools": [
119 |         "browserbase_session_create",
120 |         "browserbase_stagehand_navigate",
121 |         "browserbase_stagehand_get_url",
122 |         "browserbase_session_close"
123 |       ]
124 |     }
125 |   ]
126 | }
127 | 
```

--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { Stagehand } from "@browserbasehq/stagehand";
  2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  3 | import type { Config } from "../config.d.ts";
  4 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
  5 | import { listResources, readResource } from "./mcp/resources.js";
  6 | import { SessionManager } from "./sessionManager.js";
  7 | import type { MCPTool, BrowserSession } from "./types/types.js";
  8 | 
  9 | /**
 10 |  * MCP Server Context
 11 |  *
 12 |  * Central controller that connects the MCP server infrastructure with browser automation capabilities,
 13 |  * managing server instances, browser sessions, tool execution, and resource access.
 14 |  */
 15 | 
 16 | export class Context {
 17 |   public readonly config: Config;
 18 |   private server: Server;
 19 |   private sessionManager: SessionManager;
 20 | 
 21 |   // currentSessionId is a getter that delegates to SessionManager to ensure synchronization
 22 |   // This prevents desync between Context and SessionManager session tracking
 23 |   public get currentSessionId(): string {
 24 |     return this.sessionManager.getActiveSessionId();
 25 |   }
 26 | 
 27 |   constructor(server: Server, config: Config, contextId?: string) {
 28 |     this.server = server;
 29 |     this.config = config;
 30 |     this.sessionManager = new SessionManager(contextId);
 31 |   }
 32 | 
 33 |   public getServer(): Server {
 34 |     return this.server;
 35 |   }
 36 | 
 37 |   public getSessionManager(): SessionManager {
 38 |     return this.sessionManager;
 39 |   }
 40 | 
 41 |   /**
 42 |    * Gets the Stagehand instance for the current session from SessionManager
 43 |    */
 44 |   public async getStagehand(
 45 |     sessionId: string = this.currentSessionId,
 46 |   ): Promise<Stagehand> {
 47 |     const session = await this.sessionManager.getSession(
 48 |       sessionId,
 49 |       this.config,
 50 |     );
 51 |     if (!session) {
 52 |       throw new Error(`No session found for ID: ${sessionId}`);
 53 |     }
 54 |     return session.stagehand;
 55 |   }
 56 | 
 57 |   public async getActivePage(): Promise<BrowserSession["page"] | null> {
 58 |     // Get page from session manager
 59 |     const session = await this.sessionManager.getSession(
 60 |       this.currentSessionId,
 61 |       this.config,
 62 |     );
 63 |     if (session && session.page && !session.page.isClosed()) {
 64 |       return session.page;
 65 |     }
 66 | 
 67 |     return null;
 68 |   }
 69 | 
 70 |   public async getActiveBrowser(
 71 |     createIfMissing: boolean = true,
 72 |   ): Promise<BrowserSession["browser"] | null> {
 73 |     const session = await this.sessionManager.getSession(
 74 |       this.currentSessionId,
 75 |       this.config,
 76 |       createIfMissing,
 77 |     );
 78 |     if (!session || !session.browser || !session.browser.isConnected()) {
 79 |       return null;
 80 |     }
 81 |     return session.browser;
 82 |   }
 83 | 
 84 |   async run(tool: MCPTool, args: unknown): Promise<CallToolResult> {
 85 |     try {
 86 |       console.error(
 87 |         `Executing tool: ${tool.schema.name} with args: ${JSON.stringify(args)}`,
 88 |       );
 89 | 
 90 |       // Check if this tool has a handle method (new tool system)
 91 |       if ("handle" in tool && typeof tool.handle === "function") {
 92 |         const toolResult = await tool.handle(this, args);
 93 | 
 94 |         if (toolResult?.action) {
 95 |           const actionResult = await toolResult.action();
 96 |           const content = actionResult?.content || [];
 97 | 
 98 |           return {
 99 |             content: Array.isArray(content)
100 |               ? content
101 |               : [{ type: "text", text: "Action completed successfully." }],
102 |             isError: false,
103 |           };
104 |         } else {
105 |           return {
106 |             content: [
107 |               {
108 |                 type: "text",
109 |                 text: `${tool.schema.name} completed successfully.`,
110 |               },
111 |             ],
112 |             isError: false,
113 |           };
114 |         }
115 |       } else {
116 |         // Fallback for any legacy tools without handle method
117 |         throw new Error(
118 |           `Tool ${tool.schema.name} does not have a handle method`,
119 |         );
120 |       }
121 |     } catch (error) {
122 |       const errorMessage =
123 |         error instanceof Error ? error.message : String(error);
124 |       console.error(
125 |         `Tool ${tool.schema?.name || "unknown"} failed: ${errorMessage}`,
126 |       );
127 |       return {
128 |         content: [{ type: "text", text: `Error: ${errorMessage}` }],
129 |         isError: true,
130 |       };
131 |     }
132 |   }
133 | 
134 |   /**
135 |    * List resources
136 |    * Documentation: https://modelcontextprotocol.io/docs/concepts/resources
137 |    */
138 |   listResources() {
139 |     return listResources();
140 |   }
141 | 
142 |   /**
143 |    * Read a resource by URI
144 |    * Documentation: https://modelcontextprotocol.io/docs/concepts/resources
145 |    */
146 |   readResource(uri: string) {
147 |     return readResource(uri);
148 |   }
149 | }
150 | 
```

--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { Cookie } from "playwright-core";
  2 | import type { Config } from "../config.d.ts";
  3 | import { z } from "zod";
  4 | import { AvailableModelSchema } from "@browserbasehq/stagehand";
  5 | 
  6 | export type ToolCapability = "core" | string;
  7 | 
  8 | // Define Command Line Options Structure
  9 | export type CLIOptions = {
 10 |   proxies?: boolean;
 11 |   advancedStealth?: boolean;
 12 |   contextId?: string;
 13 |   persist?: boolean;
 14 |   port?: number;
 15 |   host?: string;
 16 |   cookies?: Cookie[];
 17 |   browserWidth?: number;
 18 |   browserHeight?: number;
 19 |   modelName?: z.infer<typeof AvailableModelSchema>;
 20 |   modelApiKey?: string;
 21 |   keepAlive?: boolean;
 22 |   experimental?: boolean;
 23 | };
 24 | 
 25 | // Default Configuration Values
 26 | const defaultConfig: Config = {
 27 |   browserbaseApiKey: process.env.BROWSERBASE_API_KEY ?? "",
 28 |   browserbaseProjectId: process.env.BROWSERBASE_PROJECT_ID ?? "",
 29 |   proxies: false,
 30 |   server: {
 31 |     port: undefined,
 32 |     host: undefined,
 33 |   },
 34 |   viewPort: {
 35 |     browserWidth: 1024,
 36 |     browserHeight: 768,
 37 |   },
 38 |   cookies: undefined,
 39 |   modelName: "gemini-2.0-flash", // Default Model
 40 | };
 41 | 
 42 | // Resolve final configuration by merging defaults, file config, and CLI options
 43 | export async function resolveConfig(cliOptions: CLIOptions): Promise<Config> {
 44 |   const cliConfig = await configFromCLIOptions(cliOptions);
 45 |   // Order: Defaults < File Config < CLI Overrides
 46 |   const mergedConfig = mergeConfig(defaultConfig, cliConfig);
 47 | 
 48 |   // --- Add Browserbase Env Vars ---
 49 |   if (!mergedConfig.modelApiKey) {
 50 |     mergedConfig.modelApiKey =
 51 |       process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
 52 |   }
 53 | 
 54 |   // --------------------------------
 55 | 
 56 |   // Basic validation for Browserbase keys - provide dummy values if not set
 57 |   if (!mergedConfig.browserbaseApiKey) {
 58 |     console.warn(
 59 |       "Warning: BROWSERBASE_API_KEY environment variable not set. Using dummy value.",
 60 |     );
 61 |     mergedConfig.browserbaseApiKey = "dummy-browserbase-api-key";
 62 |   }
 63 |   if (!mergedConfig.browserbaseProjectId) {
 64 |     console.warn(
 65 |       "Warning: BROWSERBASE_PROJECT_ID environment variable not set. Using dummy value.",
 66 |     );
 67 |     mergedConfig.browserbaseProjectId = "dummy-browserbase-project-id";
 68 |   }
 69 | 
 70 |   if (!mergedConfig.modelApiKey) {
 71 |     console.warn(
 72 |       "Warning: MODEL_API_KEY environment variable not set. Using dummy value.",
 73 |     );
 74 |     mergedConfig.modelApiKey = "dummy-api-key";
 75 |   }
 76 | 
 77 |   return mergedConfig;
 78 | }
 79 | 
 80 | // Create Config structure based on CLI options
 81 | export async function configFromCLIOptions(
 82 |   cliOptions: CLIOptions,
 83 | ): Promise<Config> {
 84 |   return {
 85 |     browserbaseApiKey: process.env.BROWSERBASE_API_KEY ?? "",
 86 |     browserbaseProjectId: process.env.BROWSERBASE_PROJECT_ID ?? "",
 87 |     server: {
 88 |       port: cliOptions.port,
 89 |       host: cliOptions.host,
 90 |     },
 91 |     proxies: cliOptions.proxies,
 92 |     context: {
 93 |       contextId: cliOptions.contextId,
 94 |       persist: cliOptions.persist,
 95 |     },
 96 |     viewPort: {
 97 |       browserWidth: cliOptions.browserWidth,
 98 |       browserHeight: cliOptions.browserHeight,
 99 |     },
100 |     advancedStealth: cliOptions.advancedStealth,
101 |     cookies: cliOptions.cookies,
102 |     modelName: cliOptions.modelName,
103 |     modelApiKey: cliOptions.modelApiKey,
104 |     keepAlive: cliOptions.keepAlive,
105 |     experimental: cliOptions.experimental,
106 |   };
107 | }
108 | 
109 | // Helper function to merge config objects, excluding undefined values
110 | function pickDefined<T extends object>(obj: T | undefined): Partial<T> {
111 |   if (!obj) return {};
112 |   return Object.fromEntries(
113 |     Object.entries(obj).filter(([, v]) => v !== undefined),
114 |   ) as Partial<T>;
115 | }
116 | 
117 | // Merge two configuration objects (overrides takes precedence)
118 | function mergeConfig(base: Config, overrides: Config): Config {
119 |   const baseFiltered = pickDefined(base);
120 |   const overridesFiltered = pickDefined(overrides);
121 | 
122 |   // Create the result object
123 |   const result = { ...baseFiltered } as Config;
124 | 
125 |   // For each property in overrides
126 |   for (const [key, value] of Object.entries(overridesFiltered)) {
127 |     if (key === "context" && value && result.context) {
128 |       // Special handling for context object to ensure deep merge
129 |       result.context = {
130 |         ...result.context,
131 |         ...(value as Config["context"]),
132 |       };
133 |     } else if (
134 |       value &&
135 |       typeof value === "object" &&
136 |       !Array.isArray(value) &&
137 |       result[key as keyof Config] &&
138 |       typeof result[key as keyof Config] === "object"
139 |     ) {
140 |       // Deep merge for other nested objects
141 |       result[key as keyof Config] = {
142 |         ...(result[key as keyof Config] as object),
143 |         ...value,
144 |       } as unknown;
145 |     } else {
146 |       // Simple override for primitives, arrays, etc.
147 |       result[key as keyof Config] = value as unknown;
148 |     }
149 |   }
150 | 
151 |   return result;
152 | }
153 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as dotenv from "dotenv";
  2 | dotenv.config();
  3 | 
  4 | import { randomUUID } from "crypto";
  5 | 
  6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  7 | import { z } from "zod";
  8 | import type { MCPToolsArray } from "./types/types.js";
  9 | 
 10 | import { Context } from "./context.js";
 11 | import type { Config } from "../config.d.ts";
 12 | import { TOOLS } from "./tools/index.js";
 13 | import { AvailableModelSchema } from "@browserbasehq/stagehand";
 14 | import { RESOURCE_TEMPLATES } from "./mcp/resources.js";
 15 | 
 16 | import {
 17 |   ListResourcesRequestSchema,
 18 |   ReadResourceRequestSchema,
 19 |   ListResourceTemplatesRequestSchema,
 20 | } from "@modelcontextprotocol/sdk/types.js";
 21 | 
 22 | const cookieSchema = z.object({
 23 |   name: z.string(),
 24 |   value: z.string(),
 25 |   domain: z.string(),
 26 |   path: z.string().optional(),
 27 |   expires: z.number().optional(),
 28 |   httpOnly: z.boolean().optional(),
 29 |   secure: z.boolean().optional(),
 30 |   sameSite: z.enum(["Strict", "Lax", "None"]).optional(),
 31 | });
 32 | 
 33 | // Configuration schema for Smithery - matches existing Config interface
 34 | export const configSchema = z
 35 |   .object({
 36 |     browserbaseApiKey: z.string().describe("The Browserbase API Key to use"),
 37 |     browserbaseProjectId: z
 38 |       .string()
 39 |       .describe("The Browserbase Project ID to use"),
 40 |     proxies: z
 41 |       .boolean()
 42 |       .optional()
 43 |       .describe("Whether or not to use Browserbase proxies"),
 44 |     advancedStealth: z
 45 |       .boolean()
 46 |       .optional()
 47 |       .describe(
 48 |         "Use advanced stealth mode. Only available to Browserbase Scale Plan users",
 49 |       ),
 50 |     keepAlive: z
 51 |       .boolean()
 52 |       .optional()
 53 |       .describe("Whether or not to keep the Browserbase session alive"),
 54 |     context: z
 55 |       .object({
 56 |         contextId: z
 57 |           .string()
 58 |           .optional()
 59 |           .describe("The ID of the context to use"),
 60 |         persist: z
 61 |           .boolean()
 62 |           .optional()
 63 |           .describe("Whether or not to persist the context"),
 64 |       })
 65 |       .optional(),
 66 |     viewPort: z
 67 |       .object({
 68 |         browserWidth: z
 69 |           .number()
 70 |           .optional()
 71 |           .describe("The width of the browser"),
 72 |         browserHeight: z
 73 |           .number()
 74 |           .optional()
 75 |           .describe("The height of the browser"),
 76 |       })
 77 |       .optional(),
 78 |     cookies: z
 79 |       .array(cookieSchema)
 80 |       .optional()
 81 |       .describe("Cookies to inject into the Browserbase context"),
 82 |     server: z
 83 |       .object({
 84 |         port: z
 85 |           .number()
 86 |           .optional()
 87 |           .describe("The port to listen on for SHTTP or MCP transport"),
 88 |         host: z
 89 |           .string()
 90 |           .optional()
 91 |           .describe(
 92 |             "The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces",
 93 |           ),
 94 |       })
 95 |       .optional(),
 96 |     modelName: AvailableModelSchema.optional().describe(
 97 |       "The model to use for Stagehand (default: gemini-2.0-flash)",
 98 |     ), // Already an existing Zod Enum
 99 |     modelApiKey: z
100 |       .string()
101 |       .optional()
102 |       .describe(
103 |         "API key for the custom model provider. Required when using a model other than the default gemini-2.0-flash",
104 |       ),
105 |     experimental: z
106 |       .boolean()
107 |       .optional()
108 |       .describe("Enable experimental Stagehand features"),
109 |   })
110 |   .refine(
111 |     (data) => {
112 |       // If a non-default model is explicitly specified, API key is required
113 |       if (data.modelName && data.modelName !== "gemini-2.0-flash") {
114 |         return data.modelApiKey !== undefined && data.modelApiKey.length > 0;
115 |       }
116 |       return true;
117 |     },
118 |     {
119 |       message: "modelApiKey is required when specifying a custom model",
120 |       path: ["modelApiKey"],
121 |     },
122 |   );
123 | 
124 | // Default function for Smithery
125 | export default function ({ config }: { config: z.infer<typeof configSchema> }) {
126 |   if (!config.browserbaseApiKey) {
127 |     throw new Error("browserbaseApiKey is required");
128 |   }
129 |   if (!config.browserbaseProjectId) {
130 |     throw new Error("browserbaseProjectId is required");
131 |   }
132 | 
133 |   const server = new McpServer({
134 |     name: "Browserbase MCP Server",
135 |     version: "2.2.0",
136 |     description:
137 |       "Cloud browser automation server powered by Browserbase and Stagehand. Enables LLMs to navigate websites, interact with elements, extract data, and capture screenshots using natural language commands.",
138 |     capabilities: {
139 |       resources: {
140 |         subscribe: true,
141 |         listChanged: true,
142 |       },
143 |     },
144 |   });
145 | 
146 |   const internalConfig: Config = config as Config;
147 | 
148 |   // Create the context, passing server instance and config
149 |   const contextId = randomUUID();
150 |   const context = new Context(server.server, internalConfig, contextId);
151 | 
152 |   server.server.registerCapabilities({
153 |     resources: {
154 |       subscribe: true,
155 |       listChanged: true,
156 |     },
157 |   });
158 | 
159 |   // Add resource handlers
160 |   server.server.setRequestHandler(ListResourcesRequestSchema, async () => {
161 |     return context.listResources();
162 |   });
163 | 
164 |   server.server.setRequestHandler(
165 |     ReadResourceRequestSchema,
166 |     async (request) => {
167 |       return context.readResource(request.params.uri);
168 |     },
169 |   );
170 | 
171 |   server.server.setRequestHandler(
172 |     ListResourceTemplatesRequestSchema,
173 |     async () => {
174 |       return { resourceTemplates: RESOURCE_TEMPLATES };
175 |     },
176 |   );
177 | 
178 |   const tools: MCPToolsArray = [...TOOLS];
179 | 
180 |   // Register each tool with the Smithery server
181 |   tools.forEach((tool) => {
182 |     if (tool.schema.inputSchema instanceof z.ZodObject) {
183 |       server.tool(
184 |         tool.schema.name,
185 |         tool.schema.description,
186 |         tool.schema.inputSchema.shape,
187 |         async (params: z.infer<typeof tool.schema.inputSchema>) => {
188 |           try {
189 |             const result = await context.run(tool, params);
190 |             return result;
191 |           } catch (error) {
192 |             const errorMessage =
193 |               error instanceof Error ? error.message : String(error);
194 |             process.stderr.write(
195 |               `[Smithery Error] ${new Date().toISOString()} Error running tool ${tool.schema.name}: ${errorMessage}\n`,
196 |             );
197 |             throw new Error(
198 |               `Failed to run tool '${tool.schema.name}': ${errorMessage}`,
199 |             );
200 |           }
201 |         },
202 |       );
203 |     } else {
204 |       console.warn(
205 |         `Tool "${tool.schema.name}" has an input schema that is not a ZodObject. Schema type: ${tool.schema.inputSchema.constructor.name}`,
206 |       );
207 |     }
208 |   });
209 | 
210 |   return server.server;
211 | }
212 | 
```

--------------------------------------------------------------------------------
/evals/run-evals.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env tsx
  2 | 
  3 | import { Command } from "commander";
  4 | import * as fs from "fs/promises";
  5 | import * as path from "path";
  6 | import { evaluate } from "mcpvals";
  7 | import os from "os";
  8 | import chalk from "chalk";
  9 | 
 10 | // Load environment variables from .env file
 11 | import { config } from "dotenv";
 12 | config();
 13 | 
 14 | // Types for evaluation results
 15 | interface EvaluationResult {
 16 |   workflowName: string;
 17 |   passed: boolean;
 18 |   overallScore: number;
 19 |   results: Array<{
 20 |     metric: string;
 21 |     passed: boolean;
 22 |     score: number;
 23 |     details: string;
 24 |     metadata?: Record<string, unknown>;
 25 |   }>;
 26 | }
 27 | 
 28 | interface EvaluationReport {
 29 |   config: Record<string, unknown>;
 30 |   evaluations: EvaluationResult[];
 31 |   passed: boolean;
 32 |   timestamp: Date;
 33 | }
 34 | 
 35 | interface TestResult {
 36 |   config: string;
 37 |   passed: boolean;
 38 |   score: number;
 39 |   duration: number;
 40 |   workflows: {
 41 |     name: string;
 42 |     passed: boolean;
 43 |     score: number;
 44 |   }[];
 45 | }
 46 | 
 47 | interface EvalConfig {
 48 |   workflows: Array<{ name?: string }>;
 49 |   passThreshold?: number;
 50 |   [key: string]: unknown;
 51 | }
 52 | 
 53 | const program = new Command();
 54 | 
 55 | program
 56 |   .name("browserbase-mcp-evals")
 57 |   .description("Run evaluation tests for Browserbase MCP Server")
 58 |   .version("1.0.0");
 59 | 
 60 | program
 61 |   .command("run")
 62 |   .description("Run evaluation tests")
 63 |   .option(
 64 |     "-c, --config <path>",
 65 |     "Config file path",
 66 |     "./evals/mcp-eval.config.json",
 67 |   )
 68 |   .option("-d, --debug", "Enable debug output")
 69 |   .option("-j, --json", "Output results as JSON")
 70 |   .option("-l, --llm", "Enable LLM judge")
 71 |   .option("-o, --output <path>", "Save results to file")
 72 |   .option(
 73 |     "-p, --pass-threshold <number>",
 74 |     "Minimum average score (0-1) required to pass. Can also be set via EVAL_PASS_THRESHOLD env var.",
 75 |   )
 76 |   .option("-t, --timeout <ms>", "Override timeout in milliseconds")
 77 |   .action(async (options) => {
 78 |     try {
 79 |       const startTime = Date.now();
 80 | 
 81 |       // Check for required environment variables
 82 |       const requiredEnvVars = [
 83 |         "BROWSERBASE_API_KEY",
 84 |         "BROWSERBASE_PROJECT_ID",
 85 |         "ANTHROPIC_API_KEY",
 86 |         "GEMINI_API_KEY",
 87 |       ];
 88 |       const missingVars = requiredEnvVars.filter((v) => !process.env[v]);
 89 | 
 90 |       if (missingVars.length > 0) {
 91 |         console.error(
 92 |           chalk.red(
 93 |             `Missing required environment variables: ${missingVars.join(", ")}`,
 94 |           ),
 95 |         );
 96 |         console.error(
 97 |           chalk.yellow("Please set them before running the tests."),
 98 |         );
 99 |         console.error(chalk.yellow("Example:"));
100 | 
101 |         for (const missingVar of missingVars) {
102 |           switch (missingVar) {
103 |             case "BROWSERBASE_API_KEY":
104 |               console.error(
105 |                 chalk.yellow(
106 |                   "  export BROWSERBASE_API_KEY='your_api_key_here'",
107 |                 ),
108 |               );
109 |               break;
110 |             case "BROWSERBASE_PROJECT_ID":
111 |               console.error(
112 |                 chalk.yellow(
113 |                   "  export BROWSERBASE_PROJECT_ID='your_project_id_here'",
114 |                 ),
115 |               );
116 |               break;
117 |             case "ANTHROPIC_API_KEY":
118 |               console.error(
119 |                 chalk.yellow(
120 |                   "  export ANTHROPIC_API_KEY='sk-ant-your_key_here'",
121 |                 ),
122 |               );
123 |               break;
124 |             case "GEMINI_API_KEY":
125 |               console.error(
126 |                 chalk.yellow("  export GEMINI_API_KEY='your_gemini_key_here'"),
127 |               );
128 |               break;
129 |           }
130 |         }
131 |         process.exit(1);
132 |       }
133 | 
134 |       // Check for LLM judge requirements
135 |       if (options.llm && !process.env.OPENAI_API_KEY) {
136 |         console.error(
137 |           chalk.red("LLM judge requires OPENAI_API_KEY environment variable"),
138 |         );
139 |         process.exit(1);
140 |       }
141 | 
142 |       // Resolve config path
143 |       const configPath = path.resolve(options.config);
144 | 
145 |       // Load config to get workflow count for display
146 |       const configContent = await fs.readFile(configPath, "utf-8");
147 |       const config: EvalConfig = JSON.parse(configContent);
148 | 
149 |       console.log(chalk.blue(`Running evaluation tests from: ${configPath}`));
150 |       console.log(chalk.gray(`Workflows to test: ${config.workflows.length}`));
151 | 
152 |       // Prepare evaluation options
153 |       const evalOptions = {
154 |         debug: options.debug,
155 |         reporter: (options.json ? "json" : "console") as
156 |           | "json"
157 |           | "console"
158 |           | "junit"
159 |           | undefined,
160 |         llmJudge: options.llm,
161 |         timeout: options.timeout ? parseInt(options.timeout) : undefined,
162 |       };
163 | 
164 |       console.log(
165 |         chalk.yellow(
166 |           "Parallel mode: splitting workflows and running concurrently",
167 |         ),
168 |       );
169 | 
170 |       const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "mcp-evals-"));
171 | 
172 |       const workflowFiles: string[] = [];
173 |       for (let i = 0; i < config.workflows.length; i++) {
174 |         const wf = config.workflows[i];
175 |         const wfConfig = { ...config, workflows: [wf] };
176 |         const wfPath = path.join(
177 |           tmpDir,
178 |           `workflow-${i}-${(wf.name || "unnamed").replace(/[^a-z0-9_-]/gi, "_")}.json`,
179 |         );
180 |         await fs.writeFile(wfPath, JSON.stringify(wfConfig, null, 2));
181 |         workflowFiles.push(wfPath);
182 |       }
183 | 
184 |       const reports: EvaluationReport[] = await Promise.all(
185 |         workflowFiles.map((wfPath) => evaluate(wfPath, evalOptions)),
186 |       );
187 | 
188 |       // Aggregate results
189 |       const allEvaluations = reports.flatMap((r) => r.evaluations);
190 |       const duration = Date.now() - startTime;
191 | 
192 |       // Determine pass/fail based on threshold instead of strict all-pass
193 |       const avgScore =
194 |         allEvaluations.length === 0
195 |           ? 0
196 |           : allEvaluations.reduce((sum, e) => sum + e.overallScore, 0) /
197 |             allEvaluations.length;
198 | 
199 |       const thresholdFromEnv =
200 |         (process.env.EVAL_PASS_THRESHOLD || process.env.PASS_THRESHOLD) ?? "";
201 |       const thresholdFromCli = options.passThreshold ?? "";
202 |       const thresholdFromConfig =
203 |         typeof config.passThreshold === "number"
204 |           ? String(config.passThreshold)
205 |           : "";
206 |       const threshold = (() => {
207 |         const raw = String(
208 |           thresholdFromCli || thresholdFromEnv || thresholdFromConfig,
209 |         ).trim();
210 |         const parsed = Number.parseFloat(raw);
211 |         if (!Number.isFinite(parsed)) return 0.6; // default lowered threshold
212 |         return parsed;
213 |       })();
214 | 
215 |       const passed = avgScore >= threshold;
216 | 
217 |       const finalReport: EvaluationReport = {
218 |         config: { parallel: true, source: configPath },
219 |         evaluations: allEvaluations,
220 |         passed,
221 |         timestamp: new Date(),
222 |       };
223 | 
224 |       const finalResult: TestResult = {
225 |         config: configPath,
226 |         passed,
227 |         score: avgScore,
228 |         duration,
229 |         workflows: allEvaluations.map((e) => ({
230 |           name: e.workflowName,
231 |           passed: e.passed,
232 |           score: e.overallScore,
233 |         })),
234 |       };
235 | 
236 |       // Best-effort cleanup
237 |       try {
238 |         await Promise.all(workflowFiles.map((f) => fs.unlink(f)));
239 |         await fs.rmdir(tmpDir);
240 |       } catch {
241 |         // ignore cleanup errors
242 |       }
243 | 
244 |       // Output results
245 |       if (options.json) {
246 |         console.log(JSON.stringify(finalResult, null, 2));
247 |       } else {
248 |         console.log(
249 |           chalk.green(
250 |             `\nTest execution completed in ${(finalResult.duration / 1000).toFixed(2)}s`,
251 |           ),
252 |         );
253 |         console.log(
254 |           chalk.gray(
255 |             `Threshold for pass: ${threshold.toFixed(2)} | Average score: ${finalResult.score.toFixed(3)}`,
256 |           ),
257 |         );
258 |         console.log(
259 |           chalk[finalResult.passed ? "green" : "red"](
260 |             `Overall result: ${finalResult.passed ? "PASSED" : "FAILED"} (${(finalResult.score * 100).toFixed(1)}%)`,
261 |           ),
262 |         );
263 |       }
264 | 
265 |       // Save to file if requested
266 |       if (options.output) {
267 |         await fs.writeFile(
268 |           options.output,
269 |           JSON.stringify(finalReport, null, 2),
270 |         );
271 |         console.log(chalk.gray(`Results saved to: ${options.output}`));
272 |       }
273 | 
274 |       process.exit(finalResult.passed ? 0 : 1);
275 |     } catch (error) {
276 |       console.error("Error running evaluation tests:", error);
277 |       process.exit(1);
278 |     }
279 |   });
280 | 
281 | program.parse();
282 | 
```

--------------------------------------------------------------------------------
/src/tools/session.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import type { Tool, ToolSchema, ToolResult } from "./tool.js";
  3 | import type { Context } from "../context.js";
  4 | import type { ToolActionResult } from "../types/types.js";
  5 | import { Browserbase } from "@browserbasehq/sdk";
  6 | import { createUIResource } from "@mcp-ui/server";
  7 | import type { BrowserSession } from "../types/types.js";
  8 | import { TextContent } from "@modelcontextprotocol/sdk/types.js";
  9 | 
 10 | // --- Tool: Create Session ---
 11 | const CreateSessionInputSchema = z.object({
 12 |   // Keep sessionId optional
 13 |   sessionId: z
 14 |     .string()
 15 |     .optional()
 16 |     .describe(
 17 |       "Optional session ID to use/reuse. If not provided or invalid, a new session is created.",
 18 |     ),
 19 | });
 20 | type CreateSessionInput = z.infer<typeof CreateSessionInputSchema>;
 21 | 
 22 | const createSessionSchema: ToolSchema<typeof CreateSessionInputSchema> = {
 23 |   name: "browserbase_session_create",
 24 |   description:
 25 |     "Create or reuse a Browserbase browser session and set it as active.",
 26 |   inputSchema: CreateSessionInputSchema,
 27 | };
 28 | 
 29 | // Handle function for CreateSession using SessionManager
 30 | async function handleCreateSession(
 31 |   context: Context,
 32 |   params: CreateSessionInput,
 33 | ): Promise<ToolResult> {
 34 |   const action = async (): Promise<ToolActionResult> => {
 35 |     try {
 36 |       const sessionManager = context.getSessionManager();
 37 |       const config = context.config; // Get config from context
 38 |       let targetSessionId: string;
 39 | 
 40 |       // Session ID Strategy: Use raw sessionId for both internal tracking and Browserbase operations
 41 |       // Default session uses generated ID with timestamp/UUID, user sessions use provided ID as-is
 42 |       if (params.sessionId) {
 43 |         targetSessionId = params.sessionId;
 44 |         process.stderr.write(
 45 |           `[tool.createSession] Attempting to create/assign session with specified ID: ${targetSessionId}\n`,
 46 |         );
 47 |       } else {
 48 |         targetSessionId = sessionManager.getDefaultSessionId();
 49 |       }
 50 | 
 51 |       let session: BrowserSession;
 52 |       const defaultSessionId = sessionManager.getDefaultSessionId();
 53 |       if (targetSessionId === defaultSessionId) {
 54 |         session = await sessionManager.ensureDefaultSessionInternal(config);
 55 |       } else {
 56 |         // When user provides a sessionId, we want to resume that Browserbase session
 57 |         // Note: targetSessionId is used for internal tracking in SessionManager
 58 |         // while params.sessionId is the Browserbase session ID to resume
 59 |         session = await sessionManager.createNewBrowserSession(
 60 |           targetSessionId, // Internal session ID for tracking
 61 |           config,
 62 |           params.sessionId, // Browserbase session ID to resume
 63 |         );
 64 |       }
 65 | 
 66 |       if (
 67 |         !session ||
 68 |         !session.browser ||
 69 |         !session.page ||
 70 |         !session.sessionId ||
 71 |         !session.stagehand
 72 |       ) {
 73 |         throw new Error(
 74 |           `SessionManager failed to return a valid session object with actualSessionId for ID: ${targetSessionId}`,
 75 |         );
 76 |       }
 77 | 
 78 |       // Note: No need to set context.currentSessionId - SessionManager handles this
 79 |       // and context.currentSessionId is a getter that delegates to SessionManager
 80 |       const bb = new Browserbase({
 81 |         apiKey: config.browserbaseApiKey,
 82 |       });
 83 | 
 84 |       const browserbaseSessionId = session.stagehand.browserbaseSessionID;
 85 |       if (!browserbaseSessionId) {
 86 |         throw new Error(
 87 |           "Browserbase session ID not found in Stagehand instance",
 88 |         );
 89 |       }
 90 |       const debugUrl = (await bb.sessions.debug(browserbaseSessionId))
 91 |         .debuggerFullscreenUrl;
 92 | 
 93 |       return {
 94 |         content: [
 95 |           {
 96 |             type: "text",
 97 |             text: `Browserbase Live Session View URL: https://www.browserbase.com/sessions/${browserbaseSessionId}`,
 98 |           },
 99 |           {
100 |             type: "text",
101 |             text: `Browserbase Live Debugger URL: ${debugUrl}`,
102 |           },
103 |           createUIResource({
104 |             uri: "ui://analytics-dashboard/main",
105 |             content: { type: "externalUrl", iframeUrl: debugUrl },
106 |             encoding: "text",
107 |           }) as unknown as TextContent,
108 |         ],
109 |       };
110 |     } catch (error: unknown) {
111 |       const errorMessage =
112 |         error instanceof Error ? error.message : String(error);
113 |       process.stderr.write(
114 |         `[tool.createSession] Action failed: ${errorMessage}\n`,
115 |       );
116 |       // Re-throw to be caught by Context.run's error handling for actions
117 |       throw new Error(`Failed to create Browserbase session: ${errorMessage}`);
118 |     }
119 |   };
120 | 
121 |   // Return the ToolResult structure expected by Context.run
122 |   return {
123 |     action: action,
124 |     waitForNetwork: false,
125 |   };
126 | }
127 | 
128 | // Define tool using handle
129 | const createSessionTool: Tool<typeof CreateSessionInputSchema> = {
130 |   capability: "core", // Add capability
131 |   schema: createSessionSchema,
132 |   handle: handleCreateSession,
133 | };
134 | 
135 | // --- Tool: Close Session ---
136 | const CloseSessionInputSchema = z.object({});
137 | 
138 | const closeSessionSchema: ToolSchema<typeof CloseSessionInputSchema> = {
139 |   name: "browserbase_session_close",
140 |   description:
141 |     "Close the current Browserbase session and reset the active context.",
142 |   inputSchema: CloseSessionInputSchema,
143 | };
144 | 
145 | async function handleCloseSession(context: Context): Promise<ToolResult> {
146 |   const action = async (): Promise<ToolActionResult> => {
147 |     // Store the current session ID before cleanup
148 |     const previousSessionId = context.currentSessionId;
149 |     let cleanupSuccessful = false;
150 |     let cleanupErrorMessage = "";
151 | 
152 |     // Step 1: Get session info before cleanup
153 |     let browserbaseSessionId: string | undefined;
154 |     const sessionManager = context.getSessionManager();
155 | 
156 |     try {
157 |       const session = await sessionManager.getSession(
158 |         previousSessionId,
159 |         context.config,
160 |         false,
161 |       );
162 | 
163 |       if (session && session.stagehand) {
164 |         // Store the actual Browserbase session ID for the replay URL
165 |         browserbaseSessionId = session.sessionId;
166 | 
167 |         // cleanupSession handles both closing Stagehand and cleanup (idempotent)
168 |         await sessionManager.cleanupSession(previousSessionId);
169 |         cleanupSuccessful = true;
170 |       } else {
171 |         process.stderr.write(
172 |           `[tool.closeSession] No session found for ID: ${previousSessionId || "default/unknown"}\n`,
173 |         );
174 |       }
175 |     } catch (error: unknown) {
176 |       cleanupErrorMessage =
177 |         error instanceof Error ? error.message : String(error);
178 |       process.stderr.write(
179 |         `[tool.closeSession] Error cleaning up session (ID was ${previousSessionId || "default/unknown"}): ${cleanupErrorMessage}\n`,
180 |       );
181 |     }
182 | 
183 |     // Step 2: SessionManager automatically resets to default on cleanup
184 |     // Context.currentSessionId getter will reflect the new active session
185 |     const oldContextSessionId = previousSessionId;
186 |     process.stderr.write(
187 |       `[tool.closeSession] Session context reset to default. Previous context session ID was ${oldContextSessionId || "default/unknown"}.\n`,
188 |     );
189 | 
190 |     // Step 3: Determine the result message
191 |     const defaultSessionId = sessionManager.getDefaultSessionId();
192 |     if (cleanupErrorMessage && !cleanupSuccessful) {
193 |       throw new Error(
194 |         `Failed to cleanup session (session ID was ${previousSessionId || "default/unknown"}). Error: ${cleanupErrorMessage}. Session context has been reset to default.`,
195 |       );
196 |     }
197 | 
198 |     if (cleanupSuccessful) {
199 |       let successMessage = `Browserbase session (${previousSessionId || "default"}) closed successfully. Context reset to default.`;
200 |       if (browserbaseSessionId && previousSessionId !== defaultSessionId) {
201 |         successMessage += ` View replay at https://www.browserbase.com/sessions/${browserbaseSessionId}`;
202 |       }
203 |       return { content: [{ type: "text", text: successMessage }] };
204 |     }
205 | 
206 |     // No session was found
207 |     let infoMessage =
208 |       "No active session found to close. Session context has been reset to default.";
209 |     if (previousSessionId && previousSessionId !== defaultSessionId) {
210 |       infoMessage = `No active session found for session ID '${previousSessionId}'. The context has been reset to default.`;
211 |     }
212 |     return { content: [{ type: "text", text: infoMessage }] };
213 |   };
214 | 
215 |   return {
216 |     action: action,
217 |     waitForNetwork: false,
218 |   };
219 | }
220 | 
221 | const closeSessionTool: Tool<typeof CloseSessionInputSchema> = {
222 |   capability: "core",
223 |   schema: closeSessionSchema,
224 |   handle: handleCloseSession,
225 | };
226 | 
227 | export default [createSessionTool, closeSessionTool];
228 | 
```

--------------------------------------------------------------------------------
/src/sessionManager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { BrowserContext, Stagehand } from "@browserbasehq/stagehand";
  2 | import type { Config } from "../config.d.ts";
  3 | import type { Cookie } from "playwright-core";
  4 | import { clearScreenshotsForSession } from "./mcp/resources.js";
  5 | import type { BrowserSession, CreateSessionParams } from "./types/types.js";
  6 | import { randomUUID } from "crypto";
  7 | 
  8 | /**
  9 |  * Create a configured Stagehand instance
 10 |  * This is used internally by SessionManager to initialize browser sessions
 11 |  */
 12 | 
 13 | export const createStagehandInstance = async (
 14 |   config: Config,
 15 |   params: CreateSessionParams = {},
 16 |   sessionId: string,
 17 | ): Promise<Stagehand> => {
 18 |   const apiKey = params.apiKey || config.browserbaseApiKey;
 19 |   const projectId = params.projectId || config.browserbaseProjectId;
 20 | 
 21 |   if (!apiKey || !projectId) {
 22 |     throw new Error("Browserbase API Key and Project ID are required");
 23 |   }
 24 | 
 25 |   const stagehand = new Stagehand({
 26 |     env: "BROWSERBASE",
 27 |     apiKey,
 28 |     projectId,
 29 |     modelName: params.modelName || config.modelName || "gemini-2.0-flash",
 30 |     modelClientOptions: {
 31 |       apiKey:
 32 |         config.modelApiKey ||
 33 |         process.env.GEMINI_API_KEY ||
 34 |         process.env.GOOGLE_API_KEY,
 35 |     },
 36 |     ...(params.browserbaseSessionID && {
 37 |       browserbaseSessionID: params.browserbaseSessionID,
 38 |     }),
 39 |     experimental: config.experimental ?? false,
 40 |     browserbaseSessionCreateParams: {
 41 |       projectId,
 42 |       proxies: config.proxies,
 43 |       keepAlive: config.keepAlive ?? false,
 44 |       browserSettings: {
 45 |         viewport: {
 46 |           width: config.viewPort?.browserWidth ?? 1024,
 47 |           height: config.viewPort?.browserHeight ?? 768,
 48 |         },
 49 |         context: config.context?.contextId
 50 |           ? {
 51 |               id: config.context?.contextId,
 52 |               persist: config.context?.persist ?? true,
 53 |             }
 54 |           : undefined,
 55 |         advancedStealth: config.advancedStealth ?? undefined,
 56 |       },
 57 |       userMetadata: {
 58 |         mcp: "true",
 59 |       },
 60 |     },
 61 |     logger: (logLine) => {
 62 |       console.error(`Stagehand[${sessionId}]: ${logLine.message}`);
 63 |     },
 64 |   });
 65 | 
 66 |   await stagehand.init();
 67 |   return stagehand;
 68 | };
 69 | 
 70 | /**
 71 |  * SessionManager manages browser sessions and tracks active/default sessions.
 72 |  *
 73 |  * Session ID Strategy:
 74 |  * - Default session: Uses generated ID with timestamp and UUID for uniqueness
 75 |  * - User sessions: Uses raw sessionId provided by user (no suffix added)
 76 |  * - All sessions stored in this.browsers Map with their internal ID as key
 77 |  *
 78 |  * Note: Context.currentSessionId is a getter that delegates to this.getActiveSessionId()
 79 |  * to ensure session tracking stays synchronized.
 80 |  */
 81 | 
 82 | export class SessionManager {
 83 |   private browsers: Map<string, BrowserSession>;
 84 |   private defaultBrowserSession: BrowserSession | null;
 85 |   private readonly defaultSessionId: string;
 86 |   private activeSessionId: string;
 87 |   // Mutex to prevent race condition when multiple calls try to create default session simultaneously
 88 |   private defaultSessionCreationPromise: Promise<BrowserSession> | null = null;
 89 |   // Track sessions currently being cleaned up to prevent concurrent cleanup
 90 |   private cleaningUpSessions: Set<string> = new Set();
 91 | 
 92 |   constructor(contextId?: string) {
 93 |     this.browsers = new Map();
 94 |     this.defaultBrowserSession = null;
 95 |     const uniqueId = randomUUID();
 96 |     this.defaultSessionId = `browserbase_session_${contextId || "default"}_${Date.now()}_${uniqueId}`;
 97 |     this.activeSessionId = this.defaultSessionId;
 98 |   }
 99 | 
100 |   getDefaultSessionId(): string {
101 |     return this.defaultSessionId;
102 |   }
103 | 
104 |   /**
105 |    * Sets the active session ID.
106 |    * @param id The ID of the session to set as active.
107 |    */
108 |   setActiveSessionId(id: string): void {
109 |     if (this.browsers.has(id)) {
110 |       this.activeSessionId = id;
111 |     } else if (id === this.defaultSessionId) {
112 |       // Allow setting to default ID even if session doesn't exist yet
113 |       // (it will be created on first use via ensureDefaultSessionInternal)
114 |       this.activeSessionId = id;
115 |     } else {
116 |       process.stderr.write(
117 |         `[SessionManager] WARN - Set active session failed for non-existent ID: ${id}\n`,
118 |       );
119 |     }
120 |   }
121 | 
122 |   /**
123 |    * Gets the active session ID.
124 |    * @returns The active session ID.
125 |    */
126 |   getActiveSessionId(): string {
127 |     return this.activeSessionId;
128 |   }
129 | 
130 |   /**
131 |    * Adds cookies to a browser context
132 |    * @param context Playwright browser context
133 |    * @param cookies Array of cookies to add
134 |    */
135 |   async addCookiesToContext(
136 |     context: BrowserContext,
137 |     cookies: Cookie[],
138 |   ): Promise<void> {
139 |     if (!cookies || cookies.length === 0) {
140 |       return;
141 |     }
142 | 
143 |     try {
144 |       process.stderr.write(
145 |         `[SessionManager] Adding ${cookies.length} cookies to browser context\n`,
146 |       );
147 | 
148 |       // Injecting cookies into the Browser Context
149 |       await context.addCookies(cookies);
150 |       process.stderr.write(
151 |         `[SessionManager] Successfully added cookies to browser context\n`,
152 |       );
153 |     } catch (error) {
154 |       process.stderr.write(
155 |         `[SessionManager] Error adding cookies to browser context: ${
156 |           error instanceof Error ? error.message : String(error)
157 |         }\n`,
158 |       );
159 |     }
160 |   }
161 | 
162 |   /**
163 |    * Creates a new Browserbase session using Stagehand.
164 |    * @param newSessionId - Internal session ID for tracking in SessionManager
165 |    * @param config - Configuration object
166 |    * @param resumeSessionId - Optional Browserbase session ID to resume/reuse
167 |    */
168 |   async createNewBrowserSession(
169 |     newSessionId: string,
170 |     config: Config,
171 |     resumeSessionId?: string,
172 |   ): Promise<BrowserSession> {
173 |     if (!config.browserbaseApiKey) {
174 |       throw new Error("Browserbase API Key is missing in the configuration.");
175 |     }
176 |     if (!config.browserbaseProjectId) {
177 |       throw new Error(
178 |         "Browserbase Project ID is missing in the configuration.",
179 |       );
180 |     }
181 | 
182 |     try {
183 |       process.stderr.write(
184 |         `[SessionManager] ${resumeSessionId ? "Resuming" : "Creating"} Stagehand session ${newSessionId}...\n`,
185 |       );
186 | 
187 |       // Create and initialize Stagehand instance using shared function
188 |       const stagehand = await createStagehandInstance(
189 |         config,
190 |         {
191 |           ...(resumeSessionId && { browserbaseSessionID: resumeSessionId }),
192 |         },
193 |         newSessionId,
194 |       );
195 | 
196 |       // Get the page and browser from Stagehand
197 |       const page = stagehand.page;
198 |       const browser = page.context().browser();
199 | 
200 |       if (!browser) {
201 |         throw new Error("Failed to get browser from Stagehand page context");
202 |       }
203 | 
204 |       const browserbaseSessionId = stagehand.browserbaseSessionID;
205 | 
206 |       if (!browserbaseSessionId) {
207 |         throw new Error(
208 |           "Browserbase session ID is required but was not returned by Stagehand",
209 |         );
210 |       }
211 | 
212 |       process.stderr.write(
213 |         `[SessionManager] Stagehand initialized with Browserbase session: ${browserbaseSessionId}\n`,
214 |       );
215 |       process.stderr.write(
216 |         `[SessionManager] Browserbase Live Debugger URL: https://www.browserbase.com/sessions/${browserbaseSessionId}\n`,
217 |       );
218 | 
219 |       // Set up disconnect handler
220 |       browser.on("disconnected", () => {
221 |         process.stderr.write(
222 |           `[SessionManager] Disconnected: ${newSessionId}\n`,
223 |         );
224 |         this.browsers.delete(newSessionId);
225 |         if (
226 |           this.defaultBrowserSession &&
227 |           this.defaultBrowserSession.browser === browser
228 |         ) {
229 |           process.stderr.write(
230 |             `[SessionManager] Disconnected (default): ${newSessionId}\n`,
231 |           );
232 |           this.defaultBrowserSession = null;
233 |           // Reset active session to default ID since default session needs recreation
234 |           this.setActiveSessionId(this.defaultSessionId);
235 |         }
236 |         if (
237 |           this.activeSessionId === newSessionId &&
238 |           newSessionId !== this.defaultSessionId
239 |         ) {
240 |           process.stderr.write(
241 |             `[SessionManager] WARN - Active session disconnected, resetting to default: ${newSessionId}\n`,
242 |           );
243 |           this.setActiveSessionId(this.defaultSessionId);
244 |         }
245 | 
246 |         // Purge any screenshots associated with both internal and Browserbase IDs
247 |         try {
248 |           clearScreenshotsForSession(newSessionId);
249 |           const bbId = browserbaseSessionId;
250 |           if (bbId) {
251 |             clearScreenshotsForSession(bbId);
252 |           }
253 |         } catch (err) {
254 |           process.stderr.write(
255 |             `[SessionManager] WARN - Failed to clear screenshots on disconnect for ${newSessionId}: ${
256 |               err instanceof Error ? err.message : String(err)
257 |             }\n`,
258 |           );
259 |         }
260 |       });
261 | 
262 |       // Add cookies to the context if they are provided in the config
263 |       if (
264 |         config.cookies &&
265 |         Array.isArray(config.cookies) &&
266 |         config.cookies.length > 0
267 |       ) {
268 |         await this.addCookiesToContext(
269 |           page.context() as BrowserContext,
270 |           config.cookies,
271 |         );
272 |       }
273 | 
274 |       const sessionObj: BrowserSession = {
275 |         browser,
276 |         page,
277 |         sessionId: browserbaseSessionId,
278 |         stagehand,
279 |       };
280 | 
281 |       this.browsers.set(newSessionId, sessionObj);
282 | 
283 |       if (newSessionId === this.defaultSessionId) {
284 |         this.defaultBrowserSession = sessionObj;
285 |       }
286 | 
287 |       this.setActiveSessionId(newSessionId);
288 |       process.stderr.write(
289 |         `[SessionManager] Session created and active: ${newSessionId}\n`,
290 |       );
291 | 
292 |       return sessionObj;
293 |     } catch (creationError) {
294 |       const errorMessage =
295 |         creationError instanceof Error
296 |           ? creationError.message
297 |           : String(creationError);
298 |       process.stderr.write(
299 |         `[SessionManager] Creating session ${newSessionId} failed: ${errorMessage}\n`,
300 |       );
301 |       throw new Error(
302 |         `Failed to create/connect session ${newSessionId}: ${errorMessage}`,
303 |       );
304 |     }
305 |   }
306 | 
307 |   private async closeBrowserGracefully(
308 |     session: BrowserSession | undefined | null,
309 |     sessionIdToLog: string,
310 |   ): Promise<void> {
311 |     // Check if this session is already being cleaned up
312 |     if (this.cleaningUpSessions.has(sessionIdToLog)) {
313 |       process.stderr.write(
314 |         `[SessionManager] Session ${sessionIdToLog} is already being cleaned up, skipping.\n`,
315 |       );
316 |       return;
317 |     }
318 | 
319 |     // Mark session as being cleaned up
320 |     this.cleaningUpSessions.add(sessionIdToLog);
321 | 
322 |     try {
323 |       // Close Stagehand instance which handles browser cleanup
324 |       if (session?.stagehand) {
325 |         try {
326 |           process.stderr.write(
327 |             `[SessionManager] Closing Stagehand for session: ${sessionIdToLog}\n`,
328 |           );
329 |           await session.stagehand.close();
330 |           process.stderr.write(
331 |             `[SessionManager] Successfully closed Stagehand and browser for session: ${sessionIdToLog}\n`,
332 |           );
333 |           // After close, purge any screenshots associated with both internal and Browserbase IDs
334 |           try {
335 |             clearScreenshotsForSession(sessionIdToLog);
336 |             const bbId = session?.stagehand?.browserbaseSessionID;
337 |             if (bbId) {
338 |               clearScreenshotsForSession(bbId);
339 |             }
340 |           } catch (err) {
341 |             process.stderr.write(
342 |               `[SessionManager] WARN - Failed to clear screenshots after close for ${sessionIdToLog}: ${
343 |                 err instanceof Error ? err.message : String(err)
344 |               }\n`,
345 |             );
346 |           }
347 |         } catch (closeError) {
348 |           process.stderr.write(
349 |             `[SessionManager] WARN - Error closing Stagehand for session ${sessionIdToLog}: ${
350 |               closeError instanceof Error
351 |                 ? closeError.message
352 |                 : String(closeError)
353 |             }\n`,
354 |           );
355 |         }
356 |       }
357 |     } finally {
358 |       // Always remove from cleanup tracking set
359 |       this.cleaningUpSessions.delete(sessionIdToLog);
360 |     }
361 |   }
362 | 
363 |   // Internal function to ensure default session
364 |   // Uses a mutex pattern to prevent race conditions when multiple calls happen concurrently
365 |   async ensureDefaultSessionInternal(config: Config): Promise<BrowserSession> {
366 |     // If a creation is already in progress, wait for it instead of starting a new one
367 |     if (this.defaultSessionCreationPromise) {
368 |       process.stderr.write(
369 |         `[SessionManager] Default session creation already in progress, waiting...\n`,
370 |       );
371 |       return await this.defaultSessionCreationPromise;
372 |     }
373 | 
374 |     const sessionId = this.defaultSessionId;
375 |     let needsReCreation = false;
376 | 
377 |     if (!this.defaultBrowserSession) {
378 |       needsReCreation = true;
379 |       process.stderr.write(
380 |         `[SessionManager] Default session ${sessionId} not found, creating.\n`,
381 |       );
382 |     } else if (
383 |       !this.defaultBrowserSession.browser.isConnected() ||
384 |       this.defaultBrowserSession.page.isClosed()
385 |     ) {
386 |       needsReCreation = true;
387 |       process.stderr.write(
388 |         `[SessionManager] Default session ${sessionId} is stale, recreating.\n`,
389 |       );
390 |       await this.closeBrowserGracefully(this.defaultBrowserSession, sessionId);
391 |       this.defaultBrowserSession = null;
392 |       this.browsers.delete(sessionId);
393 |     }
394 | 
395 |     if (needsReCreation) {
396 |       // Set the mutex promise before starting creation
397 |       this.defaultSessionCreationPromise = (async () => {
398 |         try {
399 |           this.defaultBrowserSession = await this.createNewBrowserSession(
400 |             sessionId,
401 |             config,
402 |           );
403 |           return this.defaultBrowserSession;
404 |         } catch (creationError) {
405 |           // Error during initial creation or recreation
406 |           process.stderr.write(
407 |             `[SessionManager] Initial/Recreation attempt for default session ${sessionId} failed. Error: ${
408 |               creationError instanceof Error
409 |                 ? creationError.message
410 |                 : String(creationError)
411 |             }\n`,
412 |           );
413 |           // Attempt one more time after a failure
414 |           process.stderr.write(
415 |             `[SessionManager] Retrying creation of default session ${sessionId} after error...\n`,
416 |           );
417 |           try {
418 |             this.defaultBrowserSession = await this.createNewBrowserSession(
419 |               sessionId,
420 |               config,
421 |             );
422 |             return this.defaultBrowserSession;
423 |           } catch (retryError) {
424 |             const finalErrorMessage =
425 |               retryError instanceof Error
426 |                 ? retryError.message
427 |                 : String(retryError);
428 |             process.stderr.write(
429 |               `[SessionManager] Failed to recreate default session ${sessionId} after retry: ${finalErrorMessage}\n`,
430 |             );
431 |             throw new Error(
432 |               `Failed to ensure default session ${sessionId} after initial error and retry: ${finalErrorMessage}`,
433 |             );
434 |           }
435 |         } finally {
436 |           // Clear the mutex after creation completes or fails
437 |           this.defaultSessionCreationPromise = null;
438 |         }
439 |       })();
440 | 
441 |       return await this.defaultSessionCreationPromise;
442 |     }
443 | 
444 |     // If we reached here, the existing default session is considered okay.
445 |     this.setActiveSessionId(sessionId); // Ensure default is marked active
446 |     return this.defaultBrowserSession!; // Non-null assertion: logic ensures it's not null here
447 |   }
448 | 
449 |   // Get a specific session by ID
450 |   async getSession(
451 |     sessionId: string,
452 |     config: Config,
453 |     createIfMissing: boolean = true,
454 |   ): Promise<BrowserSession | null> {
455 |     if (sessionId === this.defaultSessionId && createIfMissing) {
456 |       try {
457 |         return await this.ensureDefaultSessionInternal(config);
458 |       } catch {
459 |         process.stderr.write(
460 |           `[SessionManager] Failed to get default session due to error in ensureDefaultSessionInternal for ${sessionId}. See previous messages for details.\n`,
461 |         );
462 |         return null;
463 |       }
464 |     }
465 | 
466 |     // For non-default sessions
467 |     process.stderr.write(`[SessionManager] Getting session: ${sessionId}\n`);
468 |     const sessionObj = this.browsers.get(sessionId);
469 | 
470 |     if (!sessionObj) {
471 |       process.stderr.write(
472 |         `[SessionManager] WARN - Session not found in map: ${sessionId}\n`,
473 |       );
474 |       return null;
475 |     }
476 | 
477 |     // Validate the found session
478 |     if (!sessionObj.browser.isConnected() || sessionObj.page.isClosed()) {
479 |       process.stderr.write(
480 |         `[SessionManager] WARN - Found session ${sessionId} is stale, removing.\n`,
481 |       );
482 |       await this.closeBrowserGracefully(sessionObj, sessionId);
483 |       this.browsers.delete(sessionId);
484 |       if (this.activeSessionId === sessionId) {
485 |         process.stderr.write(
486 |           `[SessionManager] WARN - Invalidated active session ${sessionId}, resetting to default.\n`,
487 |         );
488 |         this.setActiveSessionId(this.defaultSessionId);
489 |       }
490 |       return null;
491 |     }
492 | 
493 |     // Session appears valid, make it active
494 |     this.setActiveSessionId(sessionId);
495 |     process.stderr.write(
496 |       `[SessionManager] Using valid session: ${sessionId}\n`,
497 |     );
498 |     return sessionObj;
499 |   }
500 | 
501 |   /**
502 |    * Clean up a session by closing the browser and removing it from tracking.
503 |    * This method handles both closing Stagehand and cleanup, and is idempotent.
504 |    *
505 |    * @param sessionId The session ID to clean up
506 |    */
507 |   async cleanupSession(sessionId: string): Promise<void> {
508 |     process.stderr.write(
509 |       `[SessionManager] Cleaning up session: ${sessionId}\n`,
510 |     );
511 | 
512 |     // Get the session to close it gracefully
513 |     const session = this.browsers.get(sessionId);
514 |     if (session) {
515 |       await this.closeBrowserGracefully(session, sessionId);
516 |     }
517 | 
518 |     // Remove from browsers map
519 |     this.browsers.delete(sessionId);
520 | 
521 |     // Always purge screenshots for this (internal) session id
522 |     try {
523 |       clearScreenshotsForSession(sessionId);
524 |     } catch (err) {
525 |       process.stderr.write(
526 |         `[SessionManager] WARN - Failed to clear screenshots during cleanup for ${sessionId}: ${
527 |           err instanceof Error ? err.message : String(err)
528 |         }\n`,
529 |       );
530 |     }
531 | 
532 |     // Clear default session reference if this was the default
533 |     if (sessionId === this.defaultSessionId && this.defaultBrowserSession) {
534 |       this.defaultBrowserSession = null;
535 |     }
536 | 
537 |     // Reset active session to default if this was the active one
538 |     if (this.activeSessionId === sessionId) {
539 |       process.stderr.write(
540 |         `[SessionManager] Cleaned up active session ${sessionId}, resetting to default.\n`,
541 |       );
542 |       this.setActiveSessionId(this.defaultSessionId);
543 |     }
544 |   }
545 | 
546 |   // Function to close all managed browser sessions gracefully
547 |   async closeAllSessions(): Promise<void> {
548 |     process.stderr.write(`[SessionManager] Closing all sessions...\n`);
549 |     const closePromises: Promise<void>[] = [];
550 |     for (const [id, session] of this.browsers.entries()) {
551 |       process.stderr.write(`[SessionManager] Closing session: ${id}\n`);
552 |       closePromises.push(
553 |         // Use the helper for consistent logging/error handling
554 |         this.closeBrowserGracefully(session, id),
555 |       );
556 |     }
557 |     try {
558 |       await Promise.all(closePromises);
559 |     } catch {
560 |       // Individual errors are caught and logged by closeBrowserGracefully
561 |       process.stderr.write(
562 |         `[SessionManager] WARN - Some errors occurred during batch session closing. See individual messages.\n`,
563 |       );
564 |     }
565 | 
566 |     this.browsers.clear();
567 |     this.defaultBrowserSession = null;
568 |     this.setActiveSessionId(this.defaultSessionId); // Reset active session to default
569 |     process.stderr.write(`[SessionManager] All sessions closed and cleared.\n`);
570 |   }
571 | }
572 | 
```