browser-use 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +301 -636
  2. package/dist/actor/element.d.ts +19 -0
  3. package/dist/actor/element.js +46 -0
  4. package/dist/actor/index.d.ts +4 -0
  5. package/dist/actor/index.js +4 -0
  6. package/dist/actor/mouse.d.ts +19 -0
  7. package/dist/actor/mouse.js +39 -0
  8. package/dist/actor/page.d.ts +29 -0
  9. package/dist/actor/page.js +88 -0
  10. package/dist/actor/utils.d.ts +4 -0
  11. package/dist/actor/utils.js +35 -0
  12. package/dist/agent/cloud-events.d.ts +18 -0
  13. package/dist/agent/cloud-events.js +65 -2
  14. package/dist/agent/gif.d.ts +1 -0
  15. package/dist/agent/gif.js +24 -2
  16. package/dist/agent/judge.d.ts +17 -0
  17. package/dist/agent/judge.js +197 -0
  18. package/dist/agent/message-manager/service.d.ts +12 -4
  19. package/dist/agent/message-manager/service.js +205 -39
  20. package/dist/agent/message-manager/utils.js +0 -1
  21. package/dist/agent/message-manager/views.d.ts +4 -0
  22. package/dist/agent/message-manager/views.js +11 -7
  23. package/dist/agent/prompts.d.ts +24 -3
  24. package/dist/agent/prompts.js +274 -59
  25. package/dist/agent/service.d.ts +99 -40
  26. package/dist/agent/service.js +2282 -474
  27. package/dist/agent/variable-detector.d.ts +12 -0
  28. package/dist/agent/variable-detector.js +211 -0
  29. package/dist/agent/views.d.ts +237 -17
  30. package/dist/agent/views.js +446 -32
  31. package/dist/browser/cloud/cloud.d.ts +20 -0
  32. package/dist/browser/cloud/cloud.js +129 -0
  33. package/dist/browser/cloud/index.d.ts +2 -0
  34. package/dist/browser/cloud/index.js +2 -0
  35. package/dist/browser/cloud/views.d.ts +41 -0
  36. package/dist/browser/cloud/views.js +35 -0
  37. package/dist/browser/events.d.ts +345 -0
  38. package/dist/browser/events.js +566 -0
  39. package/dist/browser/extensions.js +17 -17
  40. package/dist/browser/index.d.ts +4 -0
  41. package/dist/browser/index.js +4 -0
  42. package/dist/browser/profile.d.ts +8 -2
  43. package/dist/browser/profile.js +79 -12
  44. package/dist/browser/session-manager.d.ts +85 -0
  45. package/dist/browser/session-manager.js +208 -0
  46. package/dist/browser/session.d.ts +100 -8
  47. package/dist/browser/session.js +1102 -63
  48. package/dist/browser/types.d.ts +0 -2
  49. package/dist/browser/views.d.ts +39 -0
  50. package/dist/browser/views.js +32 -0
  51. package/dist/browser/watchdogs/aboutblank-watchdog.d.ts +12 -0
  52. package/dist/browser/watchdogs/aboutblank-watchdog.js +131 -0
  53. package/dist/browser/watchdogs/base.d.ts +21 -0
  54. package/dist/browser/watchdogs/base.js +81 -0
  55. package/dist/browser/watchdogs/cdp-session-watchdog.d.ts +14 -0
  56. package/dist/browser/watchdogs/cdp-session-watchdog.js +177 -0
  57. package/dist/browser/watchdogs/crash-watchdog.d.ts +38 -0
  58. package/dist/browser/watchdogs/crash-watchdog.js +296 -0
  59. package/dist/browser/watchdogs/default-action-watchdog.d.ts +49 -0
  60. package/dist/browser/watchdogs/default-action-watchdog.js +212 -0
  61. package/dist/browser/watchdogs/dom-watchdog.d.ts +8 -0
  62. package/dist/browser/watchdogs/dom-watchdog.js +31 -0
  63. package/dist/browser/watchdogs/downloads-watchdog.d.ts +77 -0
  64. package/dist/browser/watchdogs/downloads-watchdog.js +409 -0
  65. package/dist/browser/watchdogs/har-recording-watchdog.d.ts +19 -0
  66. package/dist/browser/watchdogs/har-recording-watchdog.js +317 -0
  67. package/dist/browser/watchdogs/index.d.ts +15 -0
  68. package/dist/browser/watchdogs/index.js +15 -0
  69. package/dist/browser/watchdogs/local-browser-watchdog.d.ts +10 -0
  70. package/dist/browser/watchdogs/local-browser-watchdog.js +32 -0
  71. package/dist/browser/watchdogs/permissions-watchdog.d.ts +8 -0
  72. package/dist/browser/watchdogs/permissions-watchdog.js +73 -0
  73. package/dist/browser/watchdogs/popups-watchdog.d.ts +13 -0
  74. package/dist/browser/watchdogs/popups-watchdog.js +77 -0
  75. package/dist/browser/watchdogs/recording-watchdog.d.ts +27 -0
  76. package/dist/browser/watchdogs/recording-watchdog.js +249 -0
  77. package/dist/browser/watchdogs/screenshot-watchdog.d.ts +6 -0
  78. package/dist/browser/watchdogs/screenshot-watchdog.js +13 -0
  79. package/dist/browser/watchdogs/security-watchdog.d.ts +10 -0
  80. package/dist/browser/watchdogs/security-watchdog.js +84 -0
  81. package/dist/browser/watchdogs/storage-state-watchdog.d.ts +24 -0
  82. package/dist/browser/watchdogs/storage-state-watchdog.js +288 -0
  83. package/dist/cli.d.ts +41 -0
  84. package/dist/cli.js +820 -10
  85. package/dist/code-use/formatting.d.ts +3 -0
  86. package/dist/code-use/formatting.js +18 -0
  87. package/dist/code-use/index.d.ts +6 -0
  88. package/dist/code-use/index.js +6 -0
  89. package/dist/code-use/namespace.d.ts +5 -0
  90. package/dist/code-use/namespace.js +81 -0
  91. package/dist/code-use/notebook-export.d.ts +3 -0
  92. package/dist/code-use/notebook-export.js +56 -0
  93. package/dist/code-use/service.d.ts +24 -0
  94. package/dist/code-use/service.js +104 -0
  95. package/dist/code-use/utils.d.ts +4 -0
  96. package/dist/code-use/utils.js +98 -0
  97. package/dist/code-use/views.d.ts +108 -0
  98. package/dist/code-use/views.js +165 -0
  99. package/dist/config.d.ts +13 -0
  100. package/dist/config.js +69 -3
  101. package/dist/controller/registry/service.d.ts +10 -1
  102. package/dist/controller/registry/service.js +266 -10
  103. package/dist/controller/registry/views.d.ts +4 -1
  104. package/dist/controller/registry/views.js +25 -2
  105. package/dist/controller/service.d.ts +10 -1
  106. package/dist/controller/service.js +1849 -288
  107. package/dist/controller/views.d.ts +78 -155
  108. package/dist/controller/views.js +61 -12
  109. package/dist/dom/history-tree-processor/service.d.ts +5 -0
  110. package/dist/dom/history-tree-processor/service.js +169 -14
  111. package/dist/dom/history-tree-processor/view.d.ts +7 -1
  112. package/dist/dom/history-tree-processor/view.js +10 -1
  113. package/dist/dom/markdown-extractor.d.ts +37 -0
  114. package/dist/dom/markdown-extractor.js +345 -0
  115. package/dist/dom/service.d.ts +3 -1
  116. package/dist/dom/service.js +76 -0
  117. package/dist/dom/views.d.ts +1 -0
  118. package/dist/dom/views.js +45 -0
  119. package/dist/event-bus.d.ts +107 -7
  120. package/dist/event-bus.js +313 -10
  121. package/dist/filesystem/file-system.d.ts +18 -0
  122. package/dist/filesystem/file-system.js +530 -42
  123. package/dist/index.d.ts +7 -0
  124. package/dist/index.js +6 -0
  125. package/dist/integrations/gmail/actions.d.ts +3 -3
  126. package/dist/integrations/gmail/actions.js +5 -5
  127. package/dist/llm/anthropic/chat.d.ts +18 -1
  128. package/dist/llm/anthropic/chat.js +123 -55
  129. package/dist/llm/anthropic/serializer.d.ts +2 -0
  130. package/dist/llm/anthropic/serializer.js +81 -9
  131. package/dist/llm/aws/chat-anthropic.d.ts +17 -0
  132. package/dist/llm/aws/chat-anthropic.js +129 -40
  133. package/dist/llm/aws/chat-bedrock.d.ts +28 -1
  134. package/dist/llm/aws/chat-bedrock.js +161 -34
  135. package/dist/llm/aws/serializer.d.ts +13 -1
  136. package/dist/llm/aws/serializer.js +56 -17
  137. package/dist/llm/azure/chat.d.ts +53 -2
  138. package/dist/llm/azure/chat.js +366 -53
  139. package/dist/llm/base.d.ts +2 -0
  140. package/dist/llm/browser-use/chat.d.ts +40 -0
  141. package/dist/llm/browser-use/chat.js +305 -0
  142. package/dist/llm/browser-use/index.d.ts +1 -0
  143. package/dist/llm/browser-use/index.js +1 -0
  144. package/dist/llm/cerebras/chat.d.ts +39 -0
  145. package/dist/llm/cerebras/chat.js +178 -0
  146. package/dist/llm/cerebras/index.d.ts +2 -0
  147. package/dist/llm/cerebras/index.js +2 -0
  148. package/dist/llm/cerebras/serializer.d.ts +7 -0
  149. package/dist/llm/cerebras/serializer.js +82 -0
  150. package/dist/llm/deepseek/chat.d.ts +19 -2
  151. package/dist/llm/deepseek/chat.js +138 -25
  152. package/dist/llm/google/chat.d.ts +46 -2
  153. package/dist/llm/google/chat.js +268 -63
  154. package/dist/llm/google/serializer.d.ts +9 -1
  155. package/dist/llm/google/serializer.js +141 -34
  156. package/dist/llm/groq/chat.d.ts +21 -2
  157. package/dist/llm/groq/chat.js +125 -26
  158. package/dist/llm/groq/parser.js +3 -1
  159. package/dist/llm/messages.d.ts +4 -4
  160. package/dist/llm/mistral/chat.d.ts +43 -0
  161. package/dist/llm/mistral/chat.js +154 -0
  162. package/dist/llm/mistral/index.d.ts +2 -0
  163. package/dist/llm/mistral/index.js +2 -0
  164. package/dist/llm/mistral/schema.d.ts +8 -0
  165. package/dist/llm/mistral/schema.js +27 -0
  166. package/dist/llm/models.d.ts +2 -0
  167. package/dist/llm/models.js +317 -0
  168. package/dist/llm/ollama/chat.d.ts +13 -1
  169. package/dist/llm/ollama/chat.js +110 -19
  170. package/dist/llm/ollama/serializer.d.ts +1 -0
  171. package/dist/llm/ollama/serializer.js +34 -12
  172. package/dist/llm/openai/chat.d.ts +16 -0
  173. package/dist/llm/openai/chat.js +94 -44
  174. package/dist/llm/openai/like.d.ts +5 -3
  175. package/dist/llm/openai/like.js +7 -3
  176. package/dist/llm/openai/responses-serializer.d.ts +18 -0
  177. package/dist/llm/openai/responses-serializer.js +72 -0
  178. package/dist/llm/openrouter/chat.d.ts +28 -2
  179. package/dist/llm/openrouter/chat.js +115 -29
  180. package/dist/llm/schema.d.ts +11 -1
  181. package/dist/llm/schema.js +81 -1
  182. package/dist/llm/vercel/chat.d.ts +50 -0
  183. package/dist/llm/vercel/chat.js +276 -0
  184. package/dist/llm/vercel/index.d.ts +1 -0
  185. package/dist/llm/vercel/index.js +1 -0
  186. package/dist/llm/vercel/serializer.d.ts +5 -0
  187. package/dist/llm/vercel/serializer.js +7 -0
  188. package/dist/llm/views.d.ts +2 -1
  189. package/dist/llm/views.js +3 -1
  190. package/dist/logging-config.d.ts +2 -0
  191. package/dist/logging-config.js +82 -29
  192. package/dist/mcp/client.d.ts +10 -5
  193. package/dist/mcp/client.js +21 -15
  194. package/dist/mcp/controller.d.ts +42 -3
  195. package/dist/mcp/controller.js +56 -31
  196. package/dist/mcp/server.d.ts +14 -0
  197. package/dist/mcp/server.js +257 -51
  198. package/dist/observability.js +10 -4
  199. package/dist/sandbox/index.d.ts +2 -0
  200. package/dist/sandbox/index.js +2 -0
  201. package/dist/sandbox/sandbox.d.ts +19 -0
  202. package/dist/sandbox/sandbox.js +140 -0
  203. package/dist/sandbox/views.d.ts +67 -0
  204. package/dist/sandbox/views.js +121 -0
  205. package/dist/skill-cli/index.d.ts +3 -0
  206. package/dist/skill-cli/index.js +3 -0
  207. package/dist/skill-cli/protocol.d.ts +30 -0
  208. package/dist/skill-cli/protocol.js +48 -0
  209. package/dist/skill-cli/server.d.ts +11 -0
  210. package/dist/skill-cli/server.js +85 -0
  211. package/dist/skill-cli/sessions.d.ts +24 -0
  212. package/dist/skill-cli/sessions.js +47 -0
  213. package/dist/skills/index.d.ts +3 -0
  214. package/dist/skills/index.js +3 -0
  215. package/dist/skills/service.d.ts +27 -0
  216. package/dist/skills/service.js +266 -0
  217. package/dist/skills/utils.d.ts +6 -0
  218. package/dist/skills/utils.js +53 -0
  219. package/dist/skills/views.d.ts +40 -0
  220. package/dist/skills/views.js +10 -0
  221. package/dist/sync/auth.js +8 -3
  222. package/dist/sync/service.d.ts +6 -6
  223. package/dist/sync/service.js +54 -89
  224. package/dist/telemetry/views.d.ts +20 -6
  225. package/dist/telemetry/views.js +23 -5
  226. package/dist/tokens/custom-pricing.d.ts +2 -0
  227. package/dist/tokens/custom-pricing.js +22 -0
  228. package/dist/tokens/index.d.ts +2 -0
  229. package/dist/tokens/index.js +2 -0
  230. package/dist/tokens/mappings.d.ts +1 -0
  231. package/dist/tokens/mappings.js +3 -0
  232. package/dist/tokens/service.js +30 -12
  233. package/dist/tools/extraction/index.d.ts +2 -0
  234. package/dist/tools/extraction/index.js +2 -0
  235. package/dist/tools/extraction/schema-utils.d.ts +6 -0
  236. package/dist/tools/extraction/schema-utils.js +237 -0
  237. package/dist/tools/extraction/views.d.ts +7 -0
  238. package/dist/tools/index.d.ts +5 -0
  239. package/dist/tools/index.js +5 -0
  240. package/dist/tools/registry/index.d.ts +2 -0
  241. package/dist/tools/registry/index.js +2 -0
  242. package/dist/tools/registry/service.d.ts +1 -0
  243. package/dist/tools/registry/service.js +1 -0
  244. package/dist/tools/registry/views.d.ts +1 -0
  245. package/dist/tools/registry/views.js +1 -0
  246. package/dist/tools/service.d.ts +2 -0
  247. package/dist/tools/service.js +1 -0
  248. package/dist/tools/utils.d.ts +2 -0
  249. package/dist/tools/utils.js +57 -0
  250. package/dist/tools/views.d.ts +1 -0
  251. package/dist/tools/views.js +1 -0
  252. package/dist/utils.d.ts +10 -1
  253. package/dist/utils.js +70 -3
  254. package/package.json +265 -28
  255. package/dist/dom/playground/process-dom.js +0 -5
  256. package/dist/dom/playground/test-accessibility.d.ts +0 -44
  257. package/dist/dom/playground/test-accessibility.js +0 -111
  258. /package/dist/{dom/playground/process-dom.d.ts → tools/extraction/views.js} +0 -0
@@ -29,15 +29,16 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
29
29
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
30
30
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
31
31
  import { z } from 'zod';
32
- import { zodToJsonSchema } from 'zod-to-json-schema';
33
32
  import { createLogger } from '../logging-config.js';
34
33
  import { Controller as DefaultController } from '../controller/service.js';
35
34
  import { Agent } from '../agent/service.js';
36
35
  import { BrowserSession } from '../browser/session.js';
36
+ import { BrowserStateRequestEvent } from '../browser/events.js';
37
37
  import { BrowserProfile } from '../browser/profile.js';
38
38
  import { FileSystem } from '../filesystem/file-system.js';
39
- import { ChatOpenAI } from '../llm/openai/chat.js';
39
+ import { getLlmByName } from '../llm/models.js';
40
40
  import { load_browser_use_config, get_default_llm, get_default_profile, } from '../config.js';
41
+ import { zodSchemaToJsonSchema } from '../llm/schema.js';
41
42
  import { productTelemetry } from '../telemetry/service.js';
42
43
  import { MCPServerTelemetryEvent } from '../telemetry/views.js';
43
44
  import { get_browser_use_version } from '../utils.js';
@@ -64,6 +65,9 @@ export class MCPServer {
64
65
  toolExecutionCount = 0;
65
66
  errorCount = 0;
66
67
  abortController = null;
68
+ activeSessions = new Map();
69
+ sessionTimeoutMinutes = 10;
70
+ sessionCleanupInterval = null;
67
71
  constructor(name, version) {
68
72
  this.server = new Server({
69
73
  name,
@@ -75,6 +79,11 @@ export class MCPServer {
75
79
  },
76
80
  });
77
81
  this.config = load_browser_use_config();
82
+ const configuredTimeout = Number(process.env.BROWSER_USE_MCP_SESSION_TIMEOUT_MINUTES ?? '10');
83
+ this.sessionTimeoutMinutes =
84
+ Number.isFinite(configuredTimeout) && configuredTimeout > 0
85
+ ? configuredTimeout
86
+ : 10;
78
87
  this.startTime = Date.now() / 1000;
79
88
  this.setupHandlers();
80
89
  this.registerDefaultPrompts();
@@ -94,6 +103,20 @@ export class MCPServer {
94
103
  const llm = get_default_llm(this.config);
95
104
  return llm && typeof llm === 'object' ? { ...llm } : {};
96
105
  }
106
+ seedOpenAiApiKeyFromConfig(llmConfig) {
107
+ if (typeof process.env.OPENAI_API_KEY === 'string' &&
108
+ process.env.OPENAI_API_KEY.trim()) {
109
+ return;
110
+ }
111
+ const configuredApiKey = typeof llmConfig.api_key === 'string' ? llmConfig.api_key.trim() : '';
112
+ if (configuredApiKey) {
113
+ process.env.OPENAI_API_KEY = configuredApiKey;
114
+ }
115
+ }
116
+ createLlmFromModelName(modelName, llmConfig) {
117
+ this.seedOpenAiApiKeyFromConfig(llmConfig);
118
+ return getLlmByName(modelName);
119
+ }
97
120
  sanitizeProfileConfig(profileConfig) {
98
121
  const sanitized = { ...profileConfig };
99
122
  delete sanitized.id;
@@ -104,7 +127,7 @@ export class MCPServer {
104
127
  buildDirectSessionProfile(profileConfig) {
105
128
  const merged = {
106
129
  downloads_path: '~/Downloads/browser-use-mcp',
107
- wait_between_actions: 0.5,
130
+ wait_between_actions: 0.1,
108
131
  keep_alive: true,
109
132
  user_data_dir: '~/.config/browseruse/profiles/default',
110
133
  is_mobile: false,
@@ -124,6 +147,11 @@ export class MCPServer {
124
147
  .map((entry) => String(entry).trim())
125
148
  .filter(Boolean);
126
149
  }
150
+ if (Array.isArray(merged.prohibited_domains)) {
151
+ merged.prohibited_domains = merged.prohibited_domains
152
+ .map((entry) => String(entry).trim())
153
+ .filter(Boolean);
154
+ }
127
155
  return new BrowserProfile(merged);
128
156
  }
129
157
  buildRetryProfile(profileConfig, allowedDomains) {
@@ -147,6 +175,11 @@ export class MCPServer {
147
175
  .map((entry) => String(entry).trim())
148
176
  .filter(Boolean);
149
177
  }
178
+ if (Array.isArray(merged.prohibited_domains)) {
179
+ merged.prohibited_domains = merged.prohibited_domains
180
+ .map((entry) => String(entry).trim())
181
+ .filter(Boolean);
182
+ }
150
183
  return new BrowserProfile(merged);
151
184
  }
152
185
  initializeLlmForDirectTools() {
@@ -154,23 +187,15 @@ export class MCPServer {
154
187
  return;
155
188
  }
156
189
  const llmConfig = this.getDefaultLlmConfig();
157
- const configuredApiKey = typeof llmConfig.api_key === 'string' ? llmConfig.api_key.trim() : '';
158
- const envApiKey = typeof process.env.OPENAI_API_KEY === 'string'
159
- ? process.env.OPENAI_API_KEY.trim()
160
- : '';
161
- const apiKey = configuredApiKey || envApiKey;
162
- if (!apiKey) {
163
- return;
164
- }
165
190
  const model = typeof llmConfig.model === 'string' && llmConfig.model.trim()
166
191
  ? llmConfig.model.trim()
167
192
  : 'gpt-4o-mini';
168
- const temperature = typeof llmConfig.temperature === 'number' ? llmConfig.temperature : 0.7;
169
- this.llm = new ChatOpenAI({
170
- model,
171
- apiKey,
172
- temperature,
173
- });
193
+ try {
194
+ this.llm = this.createLlmFromModelName(model, llmConfig);
195
+ }
196
+ catch (error) {
197
+ logger.debug(`Skipping MCP direct-tools LLM initialization for model "${model}": ${error instanceof Error ? error.message : String(error)}`);
198
+ }
174
199
  }
175
200
  initializeFileSystem(profileConfig) {
176
201
  if (this.fileSystem) {
@@ -183,7 +208,8 @@ export class MCPServer {
183
208
  }
184
209
  formatRetryResult(history) {
185
210
  const results = [];
186
- const steps = Array.isArray(history?.history) || typeof history?.number_of_steps === 'function'
211
+ const steps = Array.isArray(history?.history) ||
212
+ typeof history?.number_of_steps === 'function'
187
213
  ? typeof history?.number_of_steps === 'function'
188
214
  ? history.number_of_steps()
189
215
  : history.history.length
@@ -211,6 +237,126 @@ export class MCPServer {
211
237
  }
212
238
  return results.join('\n');
213
239
  }
240
+ trackSession(session) {
241
+ const now = Date.now() / 1000;
242
+ const existing = this.activeSessions.get(session.id);
243
+ if (existing) {
244
+ existing.session = session;
245
+ existing.last_activity = now;
246
+ return;
247
+ }
248
+ this.activeSessions.set(session.id, {
249
+ session,
250
+ created_at: now,
251
+ last_activity: now,
252
+ });
253
+ }
254
+ updateSessionActivity(session) {
255
+ if (!session) {
256
+ return;
257
+ }
258
+ const tracked = this.activeSessions.get(session.id);
259
+ if (!tracked) {
260
+ this.trackSession(session);
261
+ return;
262
+ }
263
+ tracked.last_activity = Date.now() / 1000;
264
+ }
265
+ serializeTrackedSessions() {
266
+ const now = Date.now() / 1000;
267
+ return Array.from(this.activeSessions.entries()).map(([session_id, tracked]) => ({
268
+ session_id,
269
+ created_at: tracked.created_at,
270
+ last_activity: tracked.last_activity,
271
+ age_minutes: (now - tracked.created_at) / 60,
272
+ active: Boolean(tracked.session?.initialized),
273
+ current_session: this.browserSession?.id === session_id,
274
+ }));
275
+ }
276
+ async shutdownSession(session) {
277
+ const withKill = session;
278
+ if (typeof withKill.kill === 'function') {
279
+ await withKill.kill();
280
+ return;
281
+ }
282
+ if (typeof session.stop === 'function') {
283
+ await session.stop();
284
+ }
285
+ }
286
+ async closeSessionById(sessionId) {
287
+ const tracked = this.activeSessions.get(sessionId);
288
+ if (!tracked) {
289
+ return {
290
+ session_id: sessionId,
291
+ closed: false,
292
+ message: `Session ${sessionId} not found`,
293
+ };
294
+ }
295
+ try {
296
+ await this.shutdownSession(tracked.session);
297
+ this.activeSessions.delete(sessionId);
298
+ if (this.browserSession?.id === sessionId) {
299
+ this.browserSession = null;
300
+ }
301
+ return {
302
+ session_id: sessionId,
303
+ closed: true,
304
+ message: `Closed session ${sessionId}`,
305
+ };
306
+ }
307
+ catch (error) {
308
+ return {
309
+ session_id: sessionId,
310
+ closed: false,
311
+ message: `Failed to close session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`,
312
+ };
313
+ }
314
+ }
315
+ async closeAllTrackedSessions() {
316
+ const sessionIds = Array.from(this.activeSessions.keys());
317
+ const results = await Promise.all(sessionIds.map((sessionId) => this.closeSessionById(sessionId)));
318
+ const closedCount = results.filter((result) => result.closed).length;
319
+ return {
320
+ closed_count: closedCount,
321
+ total_count: sessionIds.length,
322
+ results,
323
+ };
324
+ }
325
+ async cleanupExpiredSessions() {
326
+ const now = Date.now() / 1000;
327
+ const timeoutSeconds = this.sessionTimeoutMinutes * 60;
328
+ const expiredSessionIds = [];
329
+ for (const [sessionId, tracked] of this.activeSessions.entries()) {
330
+ if (now - tracked.last_activity > timeoutSeconds) {
331
+ expiredSessionIds.push(sessionId);
332
+ }
333
+ }
334
+ for (const sessionId of expiredSessionIds) {
335
+ const result = await this.closeSessionById(sessionId);
336
+ if (!result.closed) {
337
+ logger.warning(result.message);
338
+ }
339
+ }
340
+ }
341
+ startSessionCleanupLoop() {
342
+ if (this.sessionCleanupInterval) {
343
+ return;
344
+ }
345
+ this.sessionCleanupInterval = setInterval(() => {
346
+ this.cleanupExpiredSessions().catch((error) => {
347
+ logger.warning(`MCP session cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
348
+ });
349
+ }, 120_000);
350
+ // Do not keep the Node process alive solely because of the cleanup loop.
351
+ this.sessionCleanupInterval.unref?.();
352
+ }
353
+ stopSessionCleanupLoop() {
354
+ if (!this.sessionCleanupInterval) {
355
+ return;
356
+ }
357
+ clearInterval(this.sessionCleanupInterval);
358
+ this.sessionCleanupInterval = null;
359
+ }
214
360
  setupHandlers() {
215
361
  // List available tools
216
362
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -314,19 +460,23 @@ export class MCPServer {
314
460
  const profileConfig = this.getDefaultProfileConfig();
315
461
  const profile = this.buildDirectSessionProfile(profileConfig);
316
462
  this.browserSession = new BrowserSession({ browser_profile: profile });
463
+ this.trackSession(this.browserSession);
317
464
  this.initializeLlmForDirectTools();
318
465
  this.initializeFileSystem(profileConfig);
319
466
  }
320
467
  if (!this.browserSession.initialized) {
321
468
  await this.browserSession.start();
322
469
  }
470
+ this.trackSession(this.browserSession);
471
+ this.updateSessionActivity(this.browserSession);
323
472
  return this.browserSession;
324
473
  }
325
474
  async executeControllerAction(actionName, args) {
326
475
  const controller = await this.ensureController();
327
476
  const browserSession = await this.ensureBrowserSession();
477
+ this.updateSessionActivity(browserSession);
328
478
  if (actionName === 'extract_structured_data' && !this.llm) {
329
- throw new Error('LLM not initialized (set OPENAI_API_KEY)');
479
+ throw new Error('LLM not initialized. Set provider API key env vars and configure BROWSER_USE_LLM_MODEL/DEFAULT_LLM to a supported model.');
330
480
  }
331
481
  return await controller.registry.execute_action(actionName, args, {
332
482
  browser_session: browserSession,
@@ -408,13 +558,26 @@ export class MCPServer {
408
558
  this.registerTool('browser_get_state', 'Get the current state of the page including interactive elements', z
409
559
  .object({
410
560
  include_screenshot: z.boolean().default(false),
561
+ include_recent_events: z.boolean().default(false),
411
562
  })
412
- .default({ include_screenshot: false }), async (args) => {
563
+ .default({ include_screenshot: false, include_recent_events: false }), async (args) => {
413
564
  const browserSession = await this.ensureBrowserSession();
414
- const state = await browserSession.get_browser_state_with_recovery({
415
- include_screenshot: Boolean(args?.include_screenshot),
416
- cache_clickable_elements_hashes: true,
417
- });
565
+ let state = null;
566
+ if (typeof browserSession.dispatch_browser_event === 'function') {
567
+ const dispatchResult = await browserSession.dispatch_browser_event(new BrowserStateRequestEvent({
568
+ include_dom: true,
569
+ include_screenshot: Boolean(args?.include_screenshot),
570
+ include_recent_events: Boolean(args?.include_recent_events),
571
+ }));
572
+ state = dispatchResult?.event?.event_result ?? null;
573
+ }
574
+ if (!state) {
575
+ state = await browserSession.get_browser_state_with_recovery({
576
+ include_screenshot: Boolean(args?.include_screenshot),
577
+ include_recent_events: Boolean(args?.include_recent_events),
578
+ cache_clickable_elements_hashes: true,
579
+ });
580
+ }
418
581
  return {
419
582
  url: state.url,
420
583
  title: state.title,
@@ -424,6 +587,10 @@ export class MCPServer {
424
587
  pixels_below: state.pixels_below,
425
588
  browser_errors: state.browser_errors,
426
589
  loading_status: state.loading_status,
590
+ recent_events: state.recent_events,
591
+ pending_network_requests: state.pending_network_requests,
592
+ pagination_buttons: state.pagination_buttons,
593
+ closed_popup_messages: state.closed_popup_messages,
427
594
  screenshot: state.screenshot,
428
595
  interactive_elements: state.element_tree.clickable_elements_to_string(),
429
596
  interactive_count: Object.keys(state.selector_map ?? {}).length,
@@ -449,16 +616,57 @@ export class MCPServer {
449
616
  const browserSession = await this.ensureBrowserSession();
450
617
  return browserSession.get_tabs_info();
451
618
  });
452
- this.registerTool('browser_switch_tab', 'Switch to a tab by index', z.object({
453
- tab_index: z.number().int(),
454
- }), async (args) => this.executeControllerAction('switch_tab', {
455
- page_id: Number(args?.tab_index),
456
- }));
457
- this.registerTool('browser_close_tab', 'Close a tab by index', z.object({
458
- tab_index: z.number().int(),
459
- }), async (args) => this.executeControllerAction('close_tab', {
460
- page_id: Number(args?.tab_index),
461
- }));
619
+ this.registerTool('browser_switch_tab', 'Switch to a tab by tab_id (or page_id/tab_index for compatibility)', z
620
+ .object({
621
+ tab_id: z.string().trim().length(4).optional(),
622
+ page_id: z.number().int().optional(),
623
+ tab_index: z.number().int().optional(),
624
+ })
625
+ .refine((value) => value.tab_id != null ||
626
+ value.page_id != null ||
627
+ value.tab_index != null, { message: 'Provide tab_id, page_id, or tab_index' }), async (args) => {
628
+ const tabId = typeof args?.tab_id === 'string' && args.tab_id.trim()
629
+ ? args.tab_id.trim()
630
+ : null;
631
+ if (tabId) {
632
+ return this.executeControllerAction('switch_tab', { tab_id: tabId });
633
+ }
634
+ const pageId = typeof args?.page_id === 'number' && Number.isFinite(args.page_id)
635
+ ? Number(args.page_id)
636
+ : Number(args?.tab_index);
637
+ return this.executeControllerAction('switch_tab', {
638
+ page_id: pageId,
639
+ });
640
+ });
641
+ this.registerTool('browser_close_tab', 'Close a tab by tab_id (or page_id/tab_index for compatibility)', z
642
+ .object({
643
+ tab_id: z.string().trim().length(4).optional(),
644
+ page_id: z.number().int().optional(),
645
+ tab_index: z.number().int().optional(),
646
+ })
647
+ .refine((value) => value.tab_id != null ||
648
+ value.page_id != null ||
649
+ value.tab_index != null, { message: 'Provide tab_id, page_id, or tab_index' }), async (args) => {
650
+ const tabId = typeof args?.tab_id === 'string' && args.tab_id.trim()
651
+ ? args.tab_id.trim()
652
+ : null;
653
+ if (tabId) {
654
+ return this.executeControllerAction('close_tab', { tab_id: tabId });
655
+ }
656
+ const pageId = typeof args?.page_id === 'number' && Number.isFinite(args.page_id)
657
+ ? Number(args.page_id)
658
+ : Number(args?.tab_index);
659
+ return this.executeControllerAction('close_tab', {
660
+ page_id: pageId,
661
+ });
662
+ });
663
+ this.registerTool('browser_list_sessions', 'List active browser sessions managed by this MCP server', z.object({}).strict(), async () => {
664
+ return this.serializeTrackedSessions();
665
+ });
666
+ this.registerTool('browser_close_session', 'Close a specific browser session by session_id', z.object({
667
+ session_id: z.string().trim().min(1),
668
+ }), async (args) => this.closeSessionById(String(args?.session_id ?? '')));
669
+ this.registerTool('browser_close_all', 'Close all active browser sessions managed by this MCP server', z.object({}).strict(), async () => this.closeAllTrackedSessions());
462
670
  this.registerTool('retry_with_browser_use_agent', 'Retry a complex task with the browser-use autonomous agent', z.object({
463
671
  task: z.string(),
464
672
  max_steps: z.number().int().optional().default(100),
@@ -479,24 +687,17 @@ export class MCPServer {
479
687
  .filter(Boolean)
480
688
  : [];
481
689
  const llmConfig = this.getDefaultLlmConfig();
482
- const configuredApiKey = typeof llmConfig.api_key === 'string' ? llmConfig.api_key.trim() : '';
483
- const envApiKey = typeof process.env.OPENAI_API_KEY === 'string'
484
- ? process.env.OPENAI_API_KEY.trim()
485
- : '';
486
- const apiKey = configuredApiKey || envApiKey;
487
- if (!apiKey) {
488
- return 'Error: OPENAI_API_KEY not set in config or environment';
489
- }
490
690
  const configuredModel = typeof llmConfig.model === 'string' && llmConfig.model.trim()
491
691
  ? llmConfig.model.trim()
492
692
  : 'gpt-4o';
493
693
  const llmModel = model || configuredModel;
494
- const temperature = typeof llmConfig.temperature === 'number' ? llmConfig.temperature : 0.7;
495
- const llm = new ChatOpenAI({
496
- model: llmModel,
497
- apiKey,
498
- temperature,
499
- });
694
+ let llm;
695
+ try {
696
+ llm = this.createLlmFromModelName(llmModel, llmConfig);
697
+ }
698
+ catch (error) {
699
+ return `Error: Failed to initialize LLM "${llmModel}": ${error instanceof Error ? error.message : String(error)}`;
700
+ }
500
701
  const profileConfig = this.getDefaultProfileConfig();
501
702
  const profile = this.buildRetryProfile(profileConfig, allowedDomains);
502
703
  const retryBrowserSession = new BrowserSession({
@@ -593,7 +794,7 @@ export class MCPServer {
593
794
  this.tools[name] = {
594
795
  description,
595
796
  inputSchema: inputSchema instanceof z.ZodType
596
- ? zodToJsonSchema(inputSchema)
797
+ ? zodSchemaToJsonSchema(inputSchema)
597
798
  : inputSchema,
598
799
  handler,
599
800
  };
@@ -621,7 +822,10 @@ export class MCPServer {
621
822
  */
622
823
  async initBrowserSession(browserSession) {
623
824
  this.browserSession = browserSession;
825
+ this.trackSession(browserSession);
624
826
  await this.browserSession.start();
827
+ this.trackSession(this.browserSession);
828
+ this.updateSessionActivity(this.browserSession);
625
829
  logger.info('Browser session initialized');
626
830
  }
627
831
  /**
@@ -641,6 +845,7 @@ export class MCPServer {
641
845
  const transport = new StdioServerTransport();
642
846
  await this.server.connect(transport);
643
847
  this.isRunning = true;
848
+ this.startSessionCleanupLoop();
644
849
  logger.info(`🔌 MCP Server started (${this.getToolCount()} tools, ${this.getPromptCount()} prompts registered)`);
645
850
  }
646
851
  catch (error) {
@@ -659,14 +864,15 @@ export class MCPServer {
659
864
  }
660
865
  try {
661
866
  this.isRunning = false;
867
+ this.stopSessionCleanupLoop();
662
868
  // Cancel any pending operations
663
869
  if (this.abortController) {
664
870
  this.abortController.abort();
665
871
  this.abortController = null;
666
872
  }
667
873
  // Close browser session if active
668
- if (this.browserSession) {
669
- await this.browserSession.stop();
874
+ if (this.activeSessions.size > 0) {
875
+ await this.closeAllTrackedSessions();
670
876
  this.browserSession = null;
671
877
  logger.info('Browser session closed');
672
878
  }
@@ -12,7 +12,7 @@ try {
12
12
  lmnrObserve = (options) => lmnr.observe(options);
13
13
  lmnrAvailable = true;
14
14
  if (process.env.BROWSER_USE_VERBOSE_OBSERVABILITY?.toLowerCase() === 'true') {
15
- logger.info('Lmnr is available for observability');
15
+ logger.debug('Lmnr is available for observability');
16
16
  }
17
17
  }
18
18
  }
@@ -20,7 +20,7 @@ catch (error) {
20
20
  lmnrObserve = null;
21
21
  lmnrAvailable = false;
22
22
  if (process.env.BROWSER_USE_VERBOSE_OBSERVABILITY?.toLowerCase() === 'true') {
23
- logger.info(`Lmnr is not available for observability (${error.message})`);
23
+ logger.debug(`Lmnr is not available for observability (${error.message})`);
24
24
  }
25
25
  }
26
26
  const isDebugModeEnv = () => process.env.LMNR_LOGGING_LEVEL?.toLowerCase() === 'debug';
@@ -34,14 +34,20 @@ const normalizeOptions = (options = {}) => ({
34
34
  ...options,
35
35
  });
36
36
  export const observe = (options = {}) => {
37
- const normalized = normalizeOptions(options);
37
+ const normalized = normalizeOptions({
38
+ tags: ['observe', 'observe_debug'],
39
+ ...options,
40
+ });
38
41
  if (lmnrAvailable && lmnrObserve) {
39
42
  return lmnrObserve(normalized);
40
43
  }
41
44
  return createNoopDecorator();
42
45
  };
43
46
  export const observeDebug = (options = {}) => {
44
- const normalized = normalizeOptions(options);
47
+ const normalized = normalizeOptions({
48
+ tags: ['observe_debug'],
49
+ ...options,
50
+ });
45
51
  if (lmnrAvailable && lmnrObserve && isDebugModeEnv()) {
46
52
  return lmnrObserve(normalized);
47
53
  }
@@ -0,0 +1,2 @@
1
+ export * from './views.js';
2
+ export * from './sandbox.js';
@@ -0,0 +1,2 @@
1
+ export * from './views.js';
2
+ export * from './sandbox.js';
@@ -0,0 +1,19 @@
1
+ import { BrowserCreatedData, ErrorData, LogData, ResultData, SandboxError } from './views.js';
2
+ export interface SandboxOptions {
3
+ api_key?: string | null;
4
+ server_url?: string | null;
5
+ log_level?: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | string;
6
+ quiet?: boolean;
7
+ headers?: Record<string, string>;
8
+ cloud_profile_id?: string | null;
9
+ cloud_proxy_country_code?: string | null;
10
+ cloud_timeout?: number | null;
11
+ fetch_impl?: typeof fetch;
12
+ on_browser_created?: (event: BrowserCreatedData) => void | Promise<void>;
13
+ on_instance_ready?: () => void | Promise<void>;
14
+ on_log?: (event: LogData) => void | Promise<void>;
15
+ on_result?: (event: ResultData) => void | Promise<void>;
16
+ on_error?: (event: ErrorData) => void | Promise<void>;
17
+ }
18
+ export declare const sandbox: (options?: SandboxOptions) => <TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult> | TResult) => (...args: TArgs) => Promise<TResult>;
19
+ export { SandboxError };
@@ -0,0 +1,140 @@
1
+ import { createLogger } from '../logging-config.js';
2
+ import { BrowserCreatedData, ErrorData, LogData, ResultData, SandboxError, SSEEvent, SSEEventType, } from './views.js';
3
+ const logger = createLogger('browser_use.sandbox');
4
+ const defaultServerUrl = 'https://sandbox.api.browser-use.com/sandbox-stream';
5
+ const maybeInvoke = async (callback, data) => {
6
+ if (!callback) {
7
+ return;
8
+ }
9
+ await callback(data);
10
+ };
11
+ const parseSSEChunks = async (response, onEvent) => {
12
+ const processLine = async (line) => {
13
+ if (!line.startsWith('data:')) {
14
+ return;
15
+ }
16
+ const jsonPayload = line.slice(5).trim();
17
+ if (!jsonPayload) {
18
+ return;
19
+ }
20
+ try {
21
+ await onEvent(SSEEvent.from_json(jsonPayload));
22
+ }
23
+ catch {
24
+ // Ignore malformed SSE entries.
25
+ }
26
+ };
27
+ if (!response.body) {
28
+ const text = await response.text();
29
+ for (const line of text.split(/\r?\n/)) {
30
+ await processLine(line);
31
+ }
32
+ return;
33
+ }
34
+ const reader = response.body.getReader();
35
+ const decoder = new TextDecoder();
36
+ let buffer = '';
37
+ while (true) {
38
+ const { done, value } = await reader.read();
39
+ if (done) {
40
+ break;
41
+ }
42
+ buffer += decoder.decode(value, { stream: true });
43
+ const lines = buffer.split(/\r?\n/);
44
+ buffer = lines.pop() ?? '';
45
+ for (const line of lines) {
46
+ await processLine(line);
47
+ }
48
+ }
49
+ if (buffer) {
50
+ await processLine(buffer);
51
+ }
52
+ };
53
+ const shouldUseRemoteSandbox = (options) => Boolean(options.server_url ||
54
+ options.api_key ||
55
+ options.cloud_profile_id ||
56
+ options.cloud_proxy_country_code ||
57
+ options.cloud_timeout);
58
+ export const sandbox = (options = {}) => (fn) => async (...args) => {
59
+ const remoteMode = shouldUseRemoteSandbox(options);
60
+ if (!remoteMode) {
61
+ return await fn(...args);
62
+ }
63
+ const apiKey = options.api_key?.trim() || process.env.BROWSER_USE_API_KEY?.trim();
64
+ if (!apiKey) {
65
+ throw new SandboxError('BROWSER_USE_API_KEY is required for remote sandbox execution');
66
+ }
67
+ const fetch_impl = options.fetch_impl ?? fetch;
68
+ const server_url = options.server_url ?? defaultServerUrl;
69
+ const payload = {
70
+ code: Buffer.from(String(fn)).toString('base64'),
71
+ args: Buffer.from(JSON.stringify(args)).toString('base64'),
72
+ env: {
73
+ LOG_LEVEL: String(options.log_level ?? 'INFO').toUpperCase(),
74
+ },
75
+ };
76
+ if (options.cloud_profile_id != null) {
77
+ payload.cloud_profile_id = options.cloud_profile_id;
78
+ }
79
+ if (options.cloud_proxy_country_code != null) {
80
+ payload.cloud_proxy_country_code = options.cloud_proxy_country_code;
81
+ }
82
+ if (options.cloud_timeout != null) {
83
+ payload.cloud_timeout = options.cloud_timeout;
84
+ }
85
+ const response = await fetch_impl(server_url, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'X-API-Key': apiKey,
89
+ 'Content-Type': 'application/json',
90
+ ...(options.headers ?? {}),
91
+ },
92
+ body: JSON.stringify(payload),
93
+ });
94
+ if (!response.ok) {
95
+ throw new SandboxError(`Sandbox request failed with status ${response.status}`);
96
+ }
97
+ let executionResult = null;
98
+ let hasResult = false;
99
+ await parseSSEChunks(response, async (event) => {
100
+ if (event.type === SSEEventType.BROWSER_CREATED &&
101
+ event.data instanceof BrowserCreatedData) {
102
+ await maybeInvoke(options.on_browser_created, event.data);
103
+ if (!options.quiet && event.data.live_url) {
104
+ logger.info(`🔗 Live URL: ${event.data.live_url}`);
105
+ }
106
+ return;
107
+ }
108
+ if (event.type === SSEEventType.INSTANCE_READY) {
109
+ await maybeInvoke(options.on_instance_ready, undefined);
110
+ return;
111
+ }
112
+ if (event.type === SSEEventType.LOG && event.data instanceof LogData) {
113
+ await maybeInvoke(options.on_log, event.data);
114
+ if (!options.quiet) {
115
+ logger.info(event.data.message);
116
+ }
117
+ return;
118
+ }
119
+ if (event.type === SSEEventType.RESULT &&
120
+ event.data instanceof ResultData) {
121
+ await maybeInvoke(options.on_result, event.data);
122
+ if (!event.data.execution_response.success) {
123
+ throw new SandboxError(`Execution failed: ${event.data.execution_response.error ?? 'unknown error'}`);
124
+ }
125
+ executionResult = event.data.execution_response.result;
126
+ hasResult = true;
127
+ return;
128
+ }
129
+ if (event.type === SSEEventType.ERROR &&
130
+ event.data instanceof ErrorData) {
131
+ await maybeInvoke(options.on_error, event.data);
132
+ throw new SandboxError(`Execution failed: ${event.data.error || 'unknown error'}`);
133
+ }
134
+ });
135
+ if (!hasResult) {
136
+ throw new SandboxError('No result received from sandbox execution');
137
+ }
138
+ return executionResult;
139
+ };
140
+ export { SandboxError };