browser-use 0.2.0 → 0.4.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 +103 -41
  26. package/dist/agent/service.js +2336 -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 +10 -4
  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 +105 -9
  47. package/dist/browser/session.js +1166 -95
  48. package/dist/browser/types.d.ts +153 -156
  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 +15 -0
  100. package/dist/config.js +109 -7
  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 +1814 -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 +109 -4
  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 +15 -0
  198. package/dist/mcp/server.js +261 -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 +116 -49
  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,26 @@ export class MCPServer {
94
103
  const llm = get_default_llm(this.config);
95
104
  return llm && typeof llm === 'object' ? { ...llm } : {};
96
105
  }
106
+ isPlaceholderOpenAiApiKey(apiKey) {
107
+ const normalized = apiKey.trim().toLowerCase();
108
+ return (normalized === 'your-openai-api-key-here' ||
109
+ normalized === 'your-openai-api-key');
110
+ }
111
+ seedOpenAiApiKeyFromConfig(llmConfig) {
112
+ if (typeof process.env.OPENAI_API_KEY === 'string' &&
113
+ process.env.OPENAI_API_KEY.trim()) {
114
+ return;
115
+ }
116
+ const configuredApiKey = typeof llmConfig.api_key === 'string' ? llmConfig.api_key.trim() : '';
117
+ if (!configuredApiKey || this.isPlaceholderOpenAiApiKey(configuredApiKey)) {
118
+ return;
119
+ }
120
+ process.env.OPENAI_API_KEY = configuredApiKey;
121
+ }
122
+ createLlmFromModelName(modelName, llmConfig) {
123
+ this.seedOpenAiApiKeyFromConfig(llmConfig);
124
+ return getLlmByName(modelName);
125
+ }
97
126
  sanitizeProfileConfig(profileConfig) {
98
127
  const sanitized = { ...profileConfig };
99
128
  delete sanitized.id;
@@ -104,7 +133,7 @@ export class MCPServer {
104
133
  buildDirectSessionProfile(profileConfig) {
105
134
  const merged = {
106
135
  downloads_path: '~/Downloads/browser-use-mcp',
107
- wait_between_actions: 0.5,
136
+ wait_between_actions: 0.1,
108
137
  keep_alive: true,
109
138
  user_data_dir: '~/.config/browseruse/profiles/default',
110
139
  is_mobile: false,
@@ -124,6 +153,11 @@ export class MCPServer {
124
153
  .map((entry) => String(entry).trim())
125
154
  .filter(Boolean);
126
155
  }
156
+ if (Array.isArray(merged.prohibited_domains)) {
157
+ merged.prohibited_domains = merged.prohibited_domains
158
+ .map((entry) => String(entry).trim())
159
+ .filter(Boolean);
160
+ }
127
161
  return new BrowserProfile(merged);
128
162
  }
129
163
  buildRetryProfile(profileConfig, allowedDomains) {
@@ -147,6 +181,11 @@ export class MCPServer {
147
181
  .map((entry) => String(entry).trim())
148
182
  .filter(Boolean);
149
183
  }
184
+ if (Array.isArray(merged.prohibited_domains)) {
185
+ merged.prohibited_domains = merged.prohibited_domains
186
+ .map((entry) => String(entry).trim())
187
+ .filter(Boolean);
188
+ }
150
189
  return new BrowserProfile(merged);
151
190
  }
152
191
  initializeLlmForDirectTools() {
@@ -154,23 +193,15 @@ export class MCPServer {
154
193
  return;
155
194
  }
156
195
  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
196
  const model = typeof llmConfig.model === 'string' && llmConfig.model.trim()
166
197
  ? llmConfig.model.trim()
167
198
  : '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
- });
199
+ try {
200
+ this.llm = this.createLlmFromModelName(model, llmConfig);
201
+ }
202
+ catch (error) {
203
+ logger.debug(`Skipping MCP direct-tools LLM initialization for model "${model}": ${error instanceof Error ? error.message : String(error)}`);
204
+ }
174
205
  }
175
206
  initializeFileSystem(profileConfig) {
176
207
  if (this.fileSystem) {
@@ -212,6 +243,126 @@ export class MCPServer {
212
243
  }
213
244
  return results.join('\n');
214
245
  }
246
+ trackSession(session) {
247
+ const now = Date.now() / 1000;
248
+ const existing = this.activeSessions.get(session.id);
249
+ if (existing) {
250
+ existing.session = session;
251
+ existing.last_activity = now;
252
+ return;
253
+ }
254
+ this.activeSessions.set(session.id, {
255
+ session,
256
+ created_at: now,
257
+ last_activity: now,
258
+ });
259
+ }
260
+ updateSessionActivity(session) {
261
+ if (!session) {
262
+ return;
263
+ }
264
+ const tracked = this.activeSessions.get(session.id);
265
+ if (!tracked) {
266
+ this.trackSession(session);
267
+ return;
268
+ }
269
+ tracked.last_activity = Date.now() / 1000;
270
+ }
271
+ serializeTrackedSessions() {
272
+ const now = Date.now() / 1000;
273
+ return Array.from(this.activeSessions.entries()).map(([session_id, tracked]) => ({
274
+ session_id,
275
+ created_at: tracked.created_at,
276
+ last_activity: tracked.last_activity,
277
+ age_minutes: (now - tracked.created_at) / 60,
278
+ active: Boolean(tracked.session?.initialized),
279
+ current_session: this.browserSession?.id === session_id,
280
+ }));
281
+ }
282
+ async shutdownSession(session) {
283
+ const withKill = session;
284
+ if (typeof withKill.kill === 'function') {
285
+ await withKill.kill();
286
+ return;
287
+ }
288
+ if (typeof session.stop === 'function') {
289
+ await session.stop();
290
+ }
291
+ }
292
+ async closeSessionById(sessionId) {
293
+ const tracked = this.activeSessions.get(sessionId);
294
+ if (!tracked) {
295
+ return {
296
+ session_id: sessionId,
297
+ closed: false,
298
+ message: `Session ${sessionId} not found`,
299
+ };
300
+ }
301
+ try {
302
+ await this.shutdownSession(tracked.session);
303
+ this.activeSessions.delete(sessionId);
304
+ if (this.browserSession?.id === sessionId) {
305
+ this.browserSession = null;
306
+ }
307
+ return {
308
+ session_id: sessionId,
309
+ closed: true,
310
+ message: `Closed session ${sessionId}`,
311
+ };
312
+ }
313
+ catch (error) {
314
+ return {
315
+ session_id: sessionId,
316
+ closed: false,
317
+ message: `Failed to close session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`,
318
+ };
319
+ }
320
+ }
321
+ async closeAllTrackedSessions() {
322
+ const sessionIds = Array.from(this.activeSessions.keys());
323
+ const results = await Promise.all(sessionIds.map((sessionId) => this.closeSessionById(sessionId)));
324
+ const closedCount = results.filter((result) => result.closed).length;
325
+ return {
326
+ closed_count: closedCount,
327
+ total_count: sessionIds.length,
328
+ results,
329
+ };
330
+ }
331
+ async cleanupExpiredSessions() {
332
+ const now = Date.now() / 1000;
333
+ const timeoutSeconds = this.sessionTimeoutMinutes * 60;
334
+ const expiredSessionIds = [];
335
+ for (const [sessionId, tracked] of this.activeSessions.entries()) {
336
+ if (now - tracked.last_activity > timeoutSeconds) {
337
+ expiredSessionIds.push(sessionId);
338
+ }
339
+ }
340
+ for (const sessionId of expiredSessionIds) {
341
+ const result = await this.closeSessionById(sessionId);
342
+ if (!result.closed) {
343
+ logger.warning(result.message);
344
+ }
345
+ }
346
+ }
347
+ startSessionCleanupLoop() {
348
+ if (this.sessionCleanupInterval) {
349
+ return;
350
+ }
351
+ this.sessionCleanupInterval = setInterval(() => {
352
+ this.cleanupExpiredSessions().catch((error) => {
353
+ logger.warning(`MCP session cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
354
+ });
355
+ }, 120_000);
356
+ // Do not keep the Node process alive solely because of the cleanup loop.
357
+ this.sessionCleanupInterval.unref?.();
358
+ }
359
+ stopSessionCleanupLoop() {
360
+ if (!this.sessionCleanupInterval) {
361
+ return;
362
+ }
363
+ clearInterval(this.sessionCleanupInterval);
364
+ this.sessionCleanupInterval = null;
365
+ }
215
366
  setupHandlers() {
216
367
  // List available tools
217
368
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -315,19 +466,23 @@ export class MCPServer {
315
466
  const profileConfig = this.getDefaultProfileConfig();
316
467
  const profile = this.buildDirectSessionProfile(profileConfig);
317
468
  this.browserSession = new BrowserSession({ browser_profile: profile });
469
+ this.trackSession(this.browserSession);
318
470
  this.initializeLlmForDirectTools();
319
471
  this.initializeFileSystem(profileConfig);
320
472
  }
321
473
  if (!this.browserSession.initialized) {
322
474
  await this.browserSession.start();
323
475
  }
476
+ this.trackSession(this.browserSession);
477
+ this.updateSessionActivity(this.browserSession);
324
478
  return this.browserSession;
325
479
  }
326
480
  async executeControllerAction(actionName, args) {
327
481
  const controller = await this.ensureController();
328
482
  const browserSession = await this.ensureBrowserSession();
483
+ this.updateSessionActivity(browserSession);
329
484
  if (actionName === 'extract_structured_data' && !this.llm) {
330
- throw new Error('LLM not initialized (set OPENAI_API_KEY)');
485
+ 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
486
  }
332
487
  return await controller.registry.execute_action(actionName, args, {
333
488
  browser_session: browserSession,
@@ -409,13 +564,26 @@ export class MCPServer {
409
564
  this.registerTool('browser_get_state', 'Get the current state of the page including interactive elements', z
410
565
  .object({
411
566
  include_screenshot: z.boolean().default(false),
567
+ include_recent_events: z.boolean().default(false),
412
568
  })
413
- .default({ include_screenshot: false }), async (args) => {
569
+ .default({ include_screenshot: false, include_recent_events: false }), async (args) => {
414
570
  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
- });
571
+ let state = null;
572
+ if (typeof browserSession.dispatch_browser_event === 'function') {
573
+ const dispatchResult = await browserSession.dispatch_browser_event(new BrowserStateRequestEvent({
574
+ include_dom: true,
575
+ include_screenshot: Boolean(args?.include_screenshot),
576
+ include_recent_events: Boolean(args?.include_recent_events),
577
+ }));
578
+ state = dispatchResult?.event?.event_result ?? null;
579
+ }
580
+ if (!state) {
581
+ state = await browserSession.get_browser_state_with_recovery({
582
+ include_screenshot: Boolean(args?.include_screenshot),
583
+ include_recent_events: Boolean(args?.include_recent_events),
584
+ cache_clickable_elements_hashes: true,
585
+ });
586
+ }
419
587
  return {
420
588
  url: state.url,
421
589
  title: state.title,
@@ -425,6 +593,10 @@ export class MCPServer {
425
593
  pixels_below: state.pixels_below,
426
594
  browser_errors: state.browser_errors,
427
595
  loading_status: state.loading_status,
596
+ recent_events: state.recent_events,
597
+ pending_network_requests: state.pending_network_requests,
598
+ pagination_buttons: state.pagination_buttons,
599
+ closed_popup_messages: state.closed_popup_messages,
428
600
  screenshot: state.screenshot,
429
601
  interactive_elements: state.element_tree.clickable_elements_to_string(),
430
602
  interactive_count: Object.keys(state.selector_map ?? {}).length,
@@ -450,16 +622,57 @@ export class MCPServer {
450
622
  const browserSession = await this.ensureBrowserSession();
451
623
  return browserSession.get_tabs_info();
452
624
  });
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
- }));
625
+ this.registerTool('browser_switch_tab', 'Switch to a tab by tab_id (or page_id/tab_index for compatibility)', z
626
+ .object({
627
+ tab_id: z.string().trim().length(4).optional(),
628
+ page_id: z.number().int().optional(),
629
+ tab_index: z.number().int().optional(),
630
+ })
631
+ .refine((value) => value.tab_id != null ||
632
+ value.page_id != null ||
633
+ value.tab_index != null, { message: 'Provide tab_id, page_id, or tab_index' }), async (args) => {
634
+ const tabId = typeof args?.tab_id === 'string' && args.tab_id.trim()
635
+ ? args.tab_id.trim()
636
+ : null;
637
+ if (tabId) {
638
+ return this.executeControllerAction('switch_tab', { tab_id: tabId });
639
+ }
640
+ const pageId = typeof args?.page_id === 'number' && Number.isFinite(args.page_id)
641
+ ? Number(args.page_id)
642
+ : Number(args?.tab_index);
643
+ return this.executeControllerAction('switch_tab', {
644
+ page_id: pageId,
645
+ });
646
+ });
647
+ this.registerTool('browser_close_tab', 'Close a tab by tab_id (or page_id/tab_index for compatibility)', z
648
+ .object({
649
+ tab_id: z.string().trim().length(4).optional(),
650
+ page_id: z.number().int().optional(),
651
+ tab_index: z.number().int().optional(),
652
+ })
653
+ .refine((value) => value.tab_id != null ||
654
+ value.page_id != null ||
655
+ value.tab_index != null, { message: 'Provide tab_id, page_id, or tab_index' }), async (args) => {
656
+ const tabId = typeof args?.tab_id === 'string' && args.tab_id.trim()
657
+ ? args.tab_id.trim()
658
+ : null;
659
+ if (tabId) {
660
+ return this.executeControllerAction('close_tab', { tab_id: tabId });
661
+ }
662
+ const pageId = typeof args?.page_id === 'number' && Number.isFinite(args.page_id)
663
+ ? Number(args.page_id)
664
+ : Number(args?.tab_index);
665
+ return this.executeControllerAction('close_tab', {
666
+ page_id: pageId,
667
+ });
668
+ });
669
+ this.registerTool('browser_list_sessions', 'List active browser sessions managed by this MCP server', z.object({}).strict(), async () => {
670
+ return this.serializeTrackedSessions();
671
+ });
672
+ this.registerTool('browser_close_session', 'Close a specific browser session by session_id', z.object({
673
+ session_id: z.string().trim().min(1),
674
+ }), async (args) => this.closeSessionById(String(args?.session_id ?? '')));
675
+ this.registerTool('browser_close_all', 'Close all active browser sessions managed by this MCP server', z.object({}).strict(), async () => this.closeAllTrackedSessions());
463
676
  this.registerTool('retry_with_browser_use_agent', 'Retry a complex task with the browser-use autonomous agent', z.object({
464
677
  task: z.string(),
465
678
  max_steps: z.number().int().optional().default(100),
@@ -480,26 +693,17 @@ export class MCPServer {
480
693
  .filter(Boolean)
481
694
  : [];
482
695
  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
696
  const configuredModel = typeof llmConfig.model === 'string' && llmConfig.model.trim()
492
697
  ? llmConfig.model.trim()
493
698
  : 'gpt-4o';
494
699
  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
- });
700
+ let llm;
701
+ try {
702
+ llm = this.createLlmFromModelName(llmModel, llmConfig);
703
+ }
704
+ catch (error) {
705
+ return `Error: Failed to initialize LLM "${llmModel}": ${error instanceof Error ? error.message : String(error)}`;
706
+ }
503
707
  const profileConfig = this.getDefaultProfileConfig();
504
708
  const profile = this.buildRetryProfile(profileConfig, allowedDomains);
505
709
  const retryBrowserSession = new BrowserSession({
@@ -596,7 +800,7 @@ export class MCPServer {
596
800
  this.tools[name] = {
597
801
  description,
598
802
  inputSchema: inputSchema instanceof z.ZodType
599
- ? zodToJsonSchema(inputSchema)
803
+ ? zodSchemaToJsonSchema(inputSchema)
600
804
  : inputSchema,
601
805
  handler,
602
806
  };
@@ -624,7 +828,10 @@ export class MCPServer {
624
828
  */
625
829
  async initBrowserSession(browserSession) {
626
830
  this.browserSession = browserSession;
831
+ this.trackSession(browserSession);
627
832
  await this.browserSession.start();
833
+ this.trackSession(this.browserSession);
834
+ this.updateSessionActivity(this.browserSession);
628
835
  logger.info('Browser session initialized');
629
836
  }
630
837
  /**
@@ -644,6 +851,7 @@ export class MCPServer {
644
851
  const transport = new StdioServerTransport();
645
852
  await this.server.connect(transport);
646
853
  this.isRunning = true;
854
+ this.startSessionCleanupLoop();
647
855
  logger.info(`🔌 MCP Server started (${this.getToolCount()} tools, ${this.getPromptCount()} prompts registered)`);
648
856
  }
649
857
  catch (error) {
@@ -662,14 +870,15 @@ export class MCPServer {
662
870
  }
663
871
  try {
664
872
  this.isRunning = false;
873
+ this.stopSessionCleanupLoop();
665
874
  // Cancel any pending operations
666
875
  if (this.abortController) {
667
876
  this.abortController.abort();
668
877
  this.abortController = null;
669
878
  }
670
879
  // Close browser session if active
671
- if (this.browserSession) {
672
- await this.browserSession.stop();
880
+ if (this.activeSessions.size > 0) {
881
+ await this.closeAllTrackedSessions();
673
882
  this.browserSession = null;
674
883
  logger.info('Browser session closed');
675
884
  }
@@ -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 };