browser-use 0.2.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 (259) hide show
  1. package/README.md +295 -686
  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 -41
  26. package/dist/agent/service.js +2266 -472
  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 -18
  30. package/dist/agent/views.js +446 -33
  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 +1097 -58
  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 +7 -2
  84. package/dist/cli.js +182 -25
  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 +1807 -268
  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/exceptions.d.ts +0 -3
  122. package/dist/exceptions.js +0 -7
  123. package/dist/filesystem/file-system.d.ts +18 -0
  124. package/dist/filesystem/file-system.js +503 -42
  125. package/dist/index.d.ts +7 -0
  126. package/dist/index.js +6 -0
  127. package/dist/integrations/gmail/actions.d.ts +3 -3
  128. package/dist/integrations/gmail/actions.js +4 -4
  129. package/dist/llm/anthropic/chat.d.ts +18 -1
  130. package/dist/llm/anthropic/chat.js +123 -55
  131. package/dist/llm/anthropic/serializer.d.ts +2 -0
  132. package/dist/llm/anthropic/serializer.js +81 -9
  133. package/dist/llm/aws/chat-anthropic.d.ts +17 -0
  134. package/dist/llm/aws/chat-anthropic.js +126 -26
  135. package/dist/llm/aws/chat-bedrock.d.ts +28 -1
  136. package/dist/llm/aws/chat-bedrock.js +161 -34
  137. package/dist/llm/aws/serializer.d.ts +13 -1
  138. package/dist/llm/aws/serializer.js +56 -17
  139. package/dist/llm/azure/chat.d.ts +53 -2
  140. package/dist/llm/azure/chat.js +366 -54
  141. package/dist/llm/base.d.ts +2 -0
  142. package/dist/llm/browser-use/chat.d.ts +40 -0
  143. package/dist/llm/browser-use/chat.js +305 -0
  144. package/dist/llm/browser-use/index.d.ts +1 -0
  145. package/dist/llm/browser-use/index.js +1 -0
  146. package/dist/llm/cerebras/chat.d.ts +39 -0
  147. package/dist/llm/cerebras/chat.js +178 -0
  148. package/dist/llm/cerebras/index.d.ts +2 -0
  149. package/dist/llm/cerebras/index.js +2 -0
  150. package/dist/llm/cerebras/serializer.d.ts +7 -0
  151. package/dist/llm/cerebras/serializer.js +82 -0
  152. package/dist/llm/deepseek/chat.d.ts +19 -2
  153. package/dist/llm/deepseek/chat.js +138 -25
  154. package/dist/llm/google/chat.d.ts +46 -2
  155. package/dist/llm/google/chat.js +267 -64
  156. package/dist/llm/google/serializer.d.ts +9 -1
  157. package/dist/llm/google/serializer.js +141 -34
  158. package/dist/llm/groq/chat.d.ts +21 -2
  159. package/dist/llm/groq/chat.js +125 -26
  160. package/dist/llm/groq/parser.js +3 -1
  161. package/dist/llm/mistral/chat.d.ts +43 -0
  162. package/dist/llm/mistral/chat.js +154 -0
  163. package/dist/llm/mistral/index.d.ts +2 -0
  164. package/dist/llm/mistral/index.js +2 -0
  165. package/dist/llm/mistral/schema.d.ts +8 -0
  166. package/dist/llm/mistral/schema.js +27 -0
  167. package/dist/llm/models.d.ts +2 -0
  168. package/dist/llm/models.js +317 -0
  169. package/dist/llm/ollama/chat.d.ts +13 -1
  170. package/dist/llm/ollama/chat.js +110 -19
  171. package/dist/llm/ollama/serializer.d.ts +1 -0
  172. package/dist/llm/ollama/serializer.js +34 -12
  173. package/dist/llm/openai/chat.d.ts +16 -0
  174. package/dist/llm/openai/chat.js +94 -44
  175. package/dist/llm/openai/like.d.ts +5 -3
  176. package/dist/llm/openai/like.js +7 -3
  177. package/dist/llm/openai/responses-serializer.d.ts +18 -0
  178. package/dist/llm/openai/responses-serializer.js +72 -0
  179. package/dist/llm/openrouter/chat.d.ts +28 -2
  180. package/dist/llm/openrouter/chat.js +115 -29
  181. package/dist/llm/schema.d.ts +11 -1
  182. package/dist/llm/schema.js +81 -1
  183. package/dist/llm/vercel/chat.d.ts +50 -0
  184. package/dist/llm/vercel/chat.js +276 -0
  185. package/dist/llm/vercel/index.d.ts +1 -0
  186. package/dist/llm/vercel/index.js +1 -0
  187. package/dist/llm/vercel/serializer.d.ts +5 -0
  188. package/dist/llm/vercel/serializer.js +7 -0
  189. package/dist/llm/views.d.ts +2 -1
  190. package/dist/llm/views.js +3 -1
  191. package/dist/logging-config.d.ts +2 -0
  192. package/dist/logging-config.js +82 -29
  193. package/dist/mcp/client.d.ts +10 -5
  194. package/dist/mcp/client.js +14 -9
  195. package/dist/mcp/controller.d.ts +42 -3
  196. package/dist/mcp/controller.js +56 -31
  197. package/dist/mcp/server.d.ts +14 -0
  198. package/dist/mcp/server.js +255 -52
  199. package/dist/observability.js +10 -4
  200. package/dist/sandbox/index.d.ts +2 -0
  201. package/dist/sandbox/index.js +2 -0
  202. package/dist/sandbox/sandbox.d.ts +19 -0
  203. package/dist/sandbox/sandbox.js +140 -0
  204. package/dist/sandbox/views.d.ts +67 -0
  205. package/dist/sandbox/views.js +121 -0
  206. package/dist/skill-cli/index.d.ts +3 -0
  207. package/dist/skill-cli/index.js +3 -0
  208. package/dist/skill-cli/protocol.d.ts +30 -0
  209. package/dist/skill-cli/protocol.js +48 -0
  210. package/dist/skill-cli/server.d.ts +11 -0
  211. package/dist/skill-cli/server.js +85 -0
  212. package/dist/skill-cli/sessions.d.ts +24 -0
  213. package/dist/skill-cli/sessions.js +47 -0
  214. package/dist/skills/index.d.ts +3 -0
  215. package/dist/skills/index.js +3 -0
  216. package/dist/skills/service.d.ts +27 -0
  217. package/dist/skills/service.js +266 -0
  218. package/dist/skills/utils.d.ts +6 -0
  219. package/dist/skills/utils.js +53 -0
  220. package/dist/skills/views.d.ts +40 -0
  221. package/dist/skills/views.js +10 -0
  222. package/dist/sync/auth.js +8 -3
  223. package/dist/sync/service.d.ts +6 -6
  224. package/dist/sync/service.js +54 -89
  225. package/dist/telemetry/views.d.ts +20 -6
  226. package/dist/telemetry/views.js +23 -5
  227. package/dist/tokens/custom-pricing.d.ts +2 -0
  228. package/dist/tokens/custom-pricing.js +22 -0
  229. package/dist/tokens/index.d.ts +2 -0
  230. package/dist/tokens/index.js +2 -0
  231. package/dist/tokens/mappings.d.ts +1 -0
  232. package/dist/tokens/mappings.js +3 -0
  233. package/dist/tokens/service.js +27 -8
  234. package/dist/tools/extraction/index.d.ts +2 -0
  235. package/dist/tools/extraction/index.js +2 -0
  236. package/dist/tools/extraction/schema-utils.d.ts +6 -0
  237. package/dist/tools/extraction/schema-utils.js +237 -0
  238. package/dist/tools/extraction/views.d.ts +7 -0
  239. package/dist/tools/index.d.ts +5 -0
  240. package/dist/tools/index.js +5 -0
  241. package/dist/tools/registry/index.d.ts +2 -0
  242. package/dist/tools/registry/index.js +2 -0
  243. package/dist/tools/registry/service.d.ts +1 -0
  244. package/dist/tools/registry/service.js +1 -0
  245. package/dist/tools/registry/views.d.ts +1 -0
  246. package/dist/tools/registry/views.js +1 -0
  247. package/dist/tools/service.d.ts +2 -0
  248. package/dist/tools/service.js +1 -0
  249. package/dist/tools/utils.d.ts +2 -0
  250. package/dist/tools/utils.js +57 -0
  251. package/dist/tools/views.d.ts +1 -0
  252. package/dist/tools/views.js +1 -0
  253. package/dist/utils.d.ts +10 -1
  254. package/dist/utils.js +70 -3
  255. package/package.json +87 -26
  256. package/dist/dom/playground/process-dom.js +0 -5
  257. package/dist/dom/playground/test-accessibility.d.ts +0 -44
  258. package/dist/dom/playground/test-accessibility.js +0 -111
  259. /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) {
@@ -212,6 +237,126 @@ export class MCPServer {
212
237
  }
213
238
  return results.join('\n');
214
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
+ }
215
360
  setupHandlers() {
216
361
  // List available tools
217
362
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -315,19 +460,23 @@ export class MCPServer {
315
460
  const profileConfig = this.getDefaultProfileConfig();
316
461
  const profile = this.buildDirectSessionProfile(profileConfig);
317
462
  this.browserSession = new BrowserSession({ browser_profile: profile });
463
+ this.trackSession(this.browserSession);
318
464
  this.initializeLlmForDirectTools();
319
465
  this.initializeFileSystem(profileConfig);
320
466
  }
321
467
  if (!this.browserSession.initialized) {
322
468
  await this.browserSession.start();
323
469
  }
470
+ this.trackSession(this.browserSession);
471
+ this.updateSessionActivity(this.browserSession);
324
472
  return this.browserSession;
325
473
  }
326
474
  async executeControllerAction(actionName, args) {
327
475
  const controller = await this.ensureController();
328
476
  const browserSession = await this.ensureBrowserSession();
477
+ this.updateSessionActivity(browserSession);
329
478
  if (actionName === 'extract_structured_data' && !this.llm) {
330
- 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.');
331
480
  }
332
481
  return await controller.registry.execute_action(actionName, args, {
333
482
  browser_session: browserSession,
@@ -409,13 +558,26 @@ export class MCPServer {
409
558
  this.registerTool('browser_get_state', 'Get the current state of the page including interactive elements', z
410
559
  .object({
411
560
  include_screenshot: z.boolean().default(false),
561
+ include_recent_events: z.boolean().default(false),
412
562
  })
413
- .default({ include_screenshot: false }), async (args) => {
563
+ .default({ include_screenshot: false, include_recent_events: false }), async (args) => {
414
564
  const browserSession = await this.ensureBrowserSession();
415
- const state = await browserSession.get_browser_state_with_recovery({
416
- include_screenshot: Boolean(args?.include_screenshot),
417
- cache_clickable_elements_hashes: true,
418
- });
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
+ }
419
581
  return {
420
582
  url: state.url,
421
583
  title: state.title,
@@ -425,6 +587,10 @@ export class MCPServer {
425
587
  pixels_below: state.pixels_below,
426
588
  browser_errors: state.browser_errors,
427
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,
428
594
  screenshot: state.screenshot,
429
595
  interactive_elements: state.element_tree.clickable_elements_to_string(),
430
596
  interactive_count: Object.keys(state.selector_map ?? {}).length,
@@ -450,16 +616,57 @@ export class MCPServer {
450
616
  const browserSession = await this.ensureBrowserSession();
451
617
  return browserSession.get_tabs_info();
452
618
  });
453
- this.registerTool('browser_switch_tab', 'Switch to a tab by index', z.object({
454
- tab_index: z.number().int(),
455
- }), async (args) => this.executeControllerAction('switch_tab', {
456
- page_id: Number(args?.tab_index),
457
- }));
458
- this.registerTool('browser_close_tab', 'Close a tab by index', z.object({
459
- tab_index: z.number().int(),
460
- }), async (args) => this.executeControllerAction('close_tab', {
461
- page_id: Number(args?.tab_index),
462
- }));
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());
463
670
  this.registerTool('retry_with_browser_use_agent', 'Retry a complex task with the browser-use autonomous agent', z.object({
464
671
  task: z.string(),
465
672
  max_steps: z.number().int().optional().default(100),
@@ -480,26 +687,17 @@ export class MCPServer {
480
687
  .filter(Boolean)
481
688
  : [];
482
689
  const llmConfig = this.getDefaultLlmConfig();
483
- const configuredApiKey = typeof llmConfig.api_key === 'string' ? llmConfig.api_key.trim() : '';
484
- const envApiKey = typeof process.env.OPENAI_API_KEY === 'string'
485
- ? process.env.OPENAI_API_KEY.trim()
486
- : '';
487
- const apiKey = configuredApiKey || envApiKey;
488
- if (!apiKey) {
489
- return 'Error: OPENAI_API_KEY not set in config or environment';
490
- }
491
690
  const configuredModel = typeof llmConfig.model === 'string' && llmConfig.model.trim()
492
691
  ? llmConfig.model.trim()
493
692
  : 'gpt-4o';
494
693
  const llmModel = model || configuredModel;
495
- const temperature = typeof llmConfig.temperature === 'number'
496
- ? llmConfig.temperature
497
- : 0.7;
498
- const llm = new ChatOpenAI({
499
- model: llmModel,
500
- apiKey,
501
- temperature,
502
- });
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
+ }
503
701
  const profileConfig = this.getDefaultProfileConfig();
504
702
  const profile = this.buildRetryProfile(profileConfig, allowedDomains);
505
703
  const retryBrowserSession = new BrowserSession({
@@ -596,7 +794,7 @@ export class MCPServer {
596
794
  this.tools[name] = {
597
795
  description,
598
796
  inputSchema: inputSchema instanceof z.ZodType
599
- ? zodToJsonSchema(inputSchema)
797
+ ? zodSchemaToJsonSchema(inputSchema)
600
798
  : inputSchema,
601
799
  handler,
602
800
  };
@@ -624,7 +822,10 @@ export class MCPServer {
624
822
  */
625
823
  async initBrowserSession(browserSession) {
626
824
  this.browserSession = browserSession;
825
+ this.trackSession(browserSession);
627
826
  await this.browserSession.start();
827
+ this.trackSession(this.browserSession);
828
+ this.updateSessionActivity(this.browserSession);
628
829
  logger.info('Browser session initialized');
629
830
  }
630
831
  /**
@@ -644,6 +845,7 @@ export class MCPServer {
644
845
  const transport = new StdioServerTransport();
645
846
  await this.server.connect(transport);
646
847
  this.isRunning = true;
848
+ this.startSessionCleanupLoop();
647
849
  logger.info(`🔌 MCP Server started (${this.getToolCount()} tools, ${this.getPromptCount()} prompts registered)`);
648
850
  }
649
851
  catch (error) {
@@ -662,14 +864,15 @@ export class MCPServer {
662
864
  }
663
865
  try {
664
866
  this.isRunning = false;
867
+ this.stopSessionCleanupLoop();
665
868
  // Cancel any pending operations
666
869
  if (this.abortController) {
667
870
  this.abortController.abort();
668
871
  this.abortController = null;
669
872
  }
670
873
  // Close browser session if active
671
- if (this.browserSession) {
672
- await this.browserSession.stop();
874
+ if (this.activeSessions.size > 0) {
875
+ await this.closeAllTrackedSessions();
673
876
  this.browserSession = null;
674
877
  logger.info('Browser session closed');
675
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 };