ai-browser 0.2.1 → 0.2.3

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 (79) hide show
  1. package/README.md +76 -7
  2. package/dist/agent/agent-loop.d.ts.map +1 -1
  3. package/dist/agent/agent-loop.js +2 -1
  4. package/dist/agent/agent-loop.js.map +1 -1
  5. package/dist/api/mcp-sse.d.ts.map +1 -1
  6. package/dist/api/mcp-sse.js +26 -8
  7. package/dist/api/mcp-sse.js.map +1 -1
  8. package/dist/api/routes.js +1 -1
  9. package/dist/api/routes.js.map +1 -1
  10. package/dist/browser/BrowserManager.d.ts.map +1 -1
  11. package/dist/browser/BrowserManager.js +3 -2
  12. package/dist/browser/BrowserManager.js.map +1 -1
  13. package/dist/browser/index.d.ts +1 -1
  14. package/dist/browser/index.d.ts.map +1 -1
  15. package/dist/cli/mcp-stdio.js +1 -1
  16. package/dist/cli/mcp-stdio.js.map +1 -1
  17. package/dist/mcp/browser-mcp-server.d.ts +6 -9
  18. package/dist/mcp/browser-mcp-server.d.ts.map +1 -1
  19. package/dist/mcp/browser-mcp-server.js +125 -218
  20. package/dist/mcp/browser-mcp-server.js.map +1 -1
  21. package/dist/mcp/task-tools.d.ts +6 -0
  22. package/dist/mcp/task-tools.d.ts.map +1 -0
  23. package/dist/mcp/task-tools.js +303 -0
  24. package/dist/mcp/task-tools.js.map +1 -0
  25. package/dist/task/artifact-store.d.ts +36 -0
  26. package/dist/task/artifact-store.d.ts.map +1 -0
  27. package/dist/task/artifact-store.js +115 -0
  28. package/dist/task/artifact-store.js.map +1 -0
  29. package/dist/task/cancel-token.d.ts +13 -0
  30. package/dist/task/cancel-token.d.ts.map +1 -0
  31. package/dist/task/cancel-token.js +42 -0
  32. package/dist/task/cancel-token.js.map +1 -0
  33. package/dist/task/error-codes.d.ts +19 -0
  34. package/dist/task/error-codes.d.ts.map +1 -0
  35. package/dist/task/error-codes.js +22 -0
  36. package/dist/task/error-codes.js.map +1 -0
  37. package/dist/task/index.d.ts +17 -0
  38. package/dist/task/index.d.ts.map +1 -0
  39. package/dist/task/index.js +10 -0
  40. package/dist/task/index.js.map +1 -0
  41. package/dist/task/run-manager.d.ts +77 -0
  42. package/dist/task/run-manager.d.ts.map +1 -0
  43. package/dist/task/run-manager.js +286 -0
  44. package/dist/task/run-manager.js.map +1 -0
  45. package/dist/task/run-store.d.ts +39 -0
  46. package/dist/task/run-store.d.ts.map +1 -0
  47. package/dist/task/run-store.js +88 -0
  48. package/dist/task/run-store.js.map +1 -0
  49. package/dist/task/templates/batch-extract.d.ts +33 -0
  50. package/dist/task/templates/batch-extract.d.ts.map +1 -0
  51. package/dist/task/templates/batch-extract.js +153 -0
  52. package/dist/task/templates/batch-extract.js.map +1 -0
  53. package/dist/task/templates/login-keep-session.d.ts +34 -0
  54. package/dist/task/templates/login-keep-session.d.ts.map +1 -0
  55. package/dist/task/templates/login-keep-session.js +190 -0
  56. package/dist/task/templates/login-keep-session.js.map +1 -0
  57. package/dist/task/templates/multi-tab-compare.d.ts +43 -0
  58. package/dist/task/templates/multi-tab-compare.d.ts.map +1 -0
  59. package/dist/task/templates/multi-tab-compare.js +204 -0
  60. package/dist/task/templates/multi-tab-compare.js.map +1 -0
  61. package/dist/task/templates/registry.d.ts +13 -0
  62. package/dist/task/templates/registry.d.ts.map +1 -0
  63. package/dist/task/templates/registry.js +40 -0
  64. package/dist/task/templates/registry.js.map +1 -0
  65. package/dist/task/tool-actions.d.ts +114 -0
  66. package/dist/task/tool-actions.d.ts.map +1 -0
  67. package/dist/task/tool-actions.js +371 -0
  68. package/dist/task/tool-actions.js.map +1 -0
  69. package/dist/task/tool-context.d.ts +26 -0
  70. package/dist/task/tool-context.d.ts.map +1 -0
  71. package/dist/task/tool-context.js +2 -0
  72. package/dist/task/tool-context.js.map +1 -0
  73. package/dist/utils/url-validator.d.ts +13 -0
  74. package/dist/utils/url-validator.d.ts.map +1 -1
  75. package/dist/utils/url-validator.js +64 -0
  76. package/dist/utils/url-validator.js.map +1 -1
  77. package/package.json +3 -2
  78. package/public/agent.html +3 -927
  79. package/public/index.html +1910 -664
@@ -3,20 +3,22 @@ import fs from 'node:fs';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { z } from 'zod';
5
5
  import { executeAction, escapeCSS, setValueByAccessibility } from '../browser/actions.js';
6
- import { validateUrl } from '../utils/url-validator.js';
7
6
  import { ElementCollector, PageAnalyzer, RegionDetector, ContentExtractor, ElementMatcher, } from '../semantic/index.js';
8
- // Structured error codes for Agent consumption
9
- export var ErrorCode;
10
- (function (ErrorCode) {
11
- ErrorCode["ELEMENT_NOT_FOUND"] = "ELEMENT_NOT_FOUND";
12
- ErrorCode["NAVIGATION_TIMEOUT"] = "NAVIGATION_TIMEOUT";
13
- ErrorCode["SESSION_NOT_FOUND"] = "SESSION_NOT_FOUND";
14
- ErrorCode["PAGE_CRASHED"] = "PAGE_CRASHED";
15
- ErrorCode["INVALID_PARAMETER"] = "INVALID_PARAMETER";
16
- ErrorCode["EXECUTION_ERROR"] = "EXECUTION_ERROR";
17
- })(ErrorCode || (ErrorCode = {}));
7
+ import * as toolActions from '../task/tool-actions.js';
8
+ import { RunManager } from '../task/run-manager.js';
9
+ import { ArtifactStore } from '../task/artifact-store.js';
10
+ import { registerTaskTools } from './task-tools.js';
11
+ export { ErrorCode } from '../task/error-codes.js';
12
+ import { ErrorCode } from '../task/error-codes.js';
18
13
  export function createBrowserMcpServer(sessionManager, cookieStore, options) {
19
14
  const server = new McpServer({ name: 'browser-mcp-server', version: '0.1.0' }, { capabilities: { tools: {} } });
15
+ // Derive URL validation options from trustLevel (with backward compat for urlValidation)
16
+ const isRemote = options?.trustLevel === 'remote';
17
+ const urlOpts = isRemote
18
+ ? { blockPrivate: true, allowFile: false }
19
+ : options?.trustLevel === 'local'
20
+ ? { allowFile: true, blockPrivate: false }
21
+ : options?.urlValidation ?? {};
20
22
  const elementCollector = new ElementCollector();
21
23
  const pageAnalyzer = new PageAnalyzer();
22
24
  const regionDetector = new RegionDetector();
@@ -70,15 +72,29 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
70
72
  // Use promise lock to prevent concurrent creation of multiple default sessions
71
73
  if (!defaultSessionPromise) {
72
74
  defaultSessionPromise = (async () => {
73
- const sessionOpts = options?.headless !== undefined ? { headless: options.headless } : {};
74
- const session = await sessionManager.create(sessionOpts);
75
- defaultSessionId = session.id;
76
- defaultSessionPromise = null;
77
- return session.id;
75
+ try {
76
+ const sessionOpts = options?.headless !== undefined ? { headless: options.headless } : {};
77
+ const session = await sessionManager.create(sessionOpts);
78
+ defaultSessionId = session.id;
79
+ defaultSessionPromise = null;
80
+ options?.onSessionCreated?.(session.id);
81
+ return session.id;
82
+ }
83
+ catch (err) {
84
+ defaultSessionPromise = null; // Reset lock to allow retry
85
+ throw err;
86
+ }
78
87
  })();
79
88
  }
80
89
  return defaultSessionPromise;
81
90
  }
91
+ // Create an isolated session for task runs (not bound to defaultSessionId)
92
+ async function createIsolatedSession() {
93
+ const sessionOpts = options?.headless !== undefined ? { headless: options.headless } : {};
94
+ const session = await sessionManager.create(sessionOpts);
95
+ options?.onSessionCreated?.(session.id);
96
+ return session.id;
97
+ }
82
98
  // Helper: get active tab for a session
83
99
  function getActiveTab(sessionId) {
84
100
  const tab = sessionManager.getActiveTab(sessionId);
@@ -118,6 +134,11 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
118
134
  return ErrorCode.SESSION_NOT_FOUND;
119
135
  return undefined;
120
136
  }
137
+ function invalidParameterError(message) {
138
+ const err = new Error(message);
139
+ err.errorCode = ErrorCode.INVALID_PARAMETER;
140
+ return err;
141
+ }
121
142
  // Helper: wrap async handler with try/catch to prevent unhandled exceptions
122
143
  function safe(fn) {
123
144
  return (async (...args) => {
@@ -130,6 +151,21 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
130
151
  }
131
152
  });
132
153
  }
154
+ // ToolContext for extracted toolActions (shared by MCP handlers and template executor)
155
+ const toolCtx = {
156
+ sessionManager,
157
+ cookieStore,
158
+ urlOpts,
159
+ trustLevel: options?.trustLevel ?? 'local',
160
+ resolveSession,
161
+ getActiveTab,
162
+ getTab: (sid, tid) => sessionManager.getTab(sid, tid),
163
+ injectCookies: injectCookiesToPage,
164
+ saveCookies: saveCookiesFromPage,
165
+ };
166
+ // RunManager + ArtifactStore for task template execution
167
+ const runManager = new RunManager();
168
+ const artifactStore = new ArtifactStore();
133
169
  // ===== create_session / close_session =====
134
170
  server.tool('create_session', '创建新的浏览器会话', {}, safe(async () => {
135
171
  const sessionOpts = options?.headless !== undefined ? { headless: options.headless } : {};
@@ -138,16 +174,23 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
138
174
  if (!defaultSessionId || !sessionManager.get(defaultSessionId)) {
139
175
  defaultSessionId = session.id;
140
176
  }
177
+ options?.onSessionCreated?.(session.id);
141
178
  return textResult({ sessionId: session.id });
142
179
  }));
143
- server.tool('close_session', '关闭浏览器会话', { sessionId: z.string().optional().describe('会话ID,不传则使用默认会话') }, safe(async ({ sessionId: rawSessionId }) => {
180
+ server.tool('close_session', '关闭浏览器会话(headful 会话会保留,不会被自动关闭)', {
181
+ sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
182
+ force: z.boolean().optional().describe('是否强制关闭 headful 会话,默认 false'),
183
+ }, safe(async ({ sessionId: rawSessionId, force }) => {
184
+ // No-op if no sessionId provided and no default session exists
185
+ if (!rawSessionId && !defaultSessionId && !defaultSessionPromise) {
186
+ return textResult({ success: true, reason: 'No active session to close' });
187
+ }
144
188
  const sessionId = await resolveSession(rawSessionId);
145
- // 关闭前保存当前会话 + 所有 headful 会话的 cookie
146
189
  await sessionManager.saveAllCookies(sessionId);
147
- // headful 会话不自动关闭,保留给用户手动操作
190
+ // 默认保留 headful 会话,force=true 时允许主动关闭。
148
191
  const session = sessionManager.get(sessionId);
149
- if (session && !session.headless) {
150
- return textResult({ success: true, kept: true, reason: 'headful session preserved' });
192
+ if (session && !session.headless && !force) {
193
+ return textResult({ success: true, kept: true, reason: 'headful session preserved (set force=true to close)' });
151
194
  }
152
195
  const closed = await sessionManager.close(sessionId);
153
196
  // Clear default session if it was closed
@@ -161,62 +204,8 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
161
204
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
162
205
  url: z.string().describe('要导航到的完整URL'),
163
206
  }, safe(async ({ sessionId: rawSessionId, url }) => {
164
- // URL validation
165
- const check = validateUrl(url, options?.urlValidation ?? {});
166
- if (!check.valid) {
167
- const err = new Error(check.reason);
168
- err.errorCode = ErrorCode.INVALID_PARAMETER;
169
- throw err;
170
- }
171
207
  const sessionId = await resolveSession(rawSessionId);
172
- const tab = getActiveTab(sessionId);
173
- // 导航前先从 headful 会话同步最新 cookie(用户可能手动登录了)
174
- await sessionManager.syncHeadfulCookies();
175
- // 注入已保存的 cookies(通过 CDP 注入全部 cookie,支持跨域 SSO)
176
- await injectCookiesToPage(tab.page);
177
- let partial = false;
178
- let statusCode;
179
- try {
180
- const response = await tab.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
181
- statusCode = response?.status();
182
- }
183
- catch (err) {
184
- if (err.name === 'TimeoutError' || err.message?.includes('timeout')) {
185
- partial = true;
186
- }
187
- else {
188
- throw new Error(err.message || 'Navigation failed');
189
- }
190
- }
191
- // 给 SPA 页面额外渲染时间
192
- try {
193
- await tab.page.waitForNetworkIdle({ timeout: 3000 });
194
- }
195
- catch {
196
- // 忽略超时,不阻塞
197
- }
198
- tab.url = tab.page.url();
199
- await saveCookiesFromPage(tab.page);
200
- sessionManager.updateActivity(sessionId);
201
- let title = '';
202
- try {
203
- title = await tab.page.title();
204
- }
205
- catch {
206
- title = '(无法获取标题)';
207
- }
208
- const result = {
209
- success: true,
210
- partial,
211
- statusCode,
212
- page: { url: tab.page.url(), title },
213
- };
214
- // Check for pending dialog after navigation
215
- if (tab.events) {
216
- const pending = tab.events.getPendingDialog();
217
- if (pending)
218
- result.dialog = pending;
219
- }
208
+ const result = await toolActions.navigate(toolCtx, sessionId, url);
220
209
  return textResult(result);
221
210
  }));
222
211
  // ===== get_page_info =====
@@ -227,66 +216,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
227
216
  }, safe(async ({ sessionId: rawSessionId, maxElements, visibleOnly }) => {
228
217
  const sessionId = await resolveSession(rawSessionId);
229
218
  const tab = getActiveTab(sessionId);
230
- const limit = maxElements ?? 50;
231
- const filterVisible = visibleOnly ?? true;
232
- const [elements, analysis, regions] = await Promise.all([
233
- elementCollector.collect(tab.page),
234
- pageAnalyzer.analyze(tab.page),
235
- regionDetector.detect(tab.page),
236
- ]);
237
- let filtered = elements;
238
- // Filter to viewport-visible elements
239
- if (filterVisible) {
240
- const viewport = tab.page.viewport();
241
- if (viewport) {
242
- filtered = filtered.filter((el) => {
243
- const b = el.bounds;
244
- if (!b || (b.width === 0 && b.height === 0))
245
- return true; // keep elements without bounds
246
- return b.y + b.height > 0 && b.y < viewport.height && b.x + b.width > 0 && b.x < viewport.width;
247
- });
248
- }
249
- }
250
- // Sort by y position and truncate
251
- const totalElements = filtered.length;
252
- filtered.sort((a, b) => (a.bounds?.y ?? 0) - (b.bounds?.y ?? 0));
253
- const truncated = filtered.length > limit;
254
- if (truncated) {
255
- filtered = filtered.slice(0, limit);
256
- }
257
- // Mask sensitive field values
258
- for (const el of filtered) {
259
- if (el.type === 'textbox' || el.type === 'input') {
260
- const idLower = (el.id || '').toLowerCase();
261
- const labelLower = (el.label || '').toLowerCase();
262
- const isPassword = idLower.includes('password') || idLower.includes('secret') || idLower.includes('token')
263
- || labelLower.includes('password') || labelLower.includes('secret') || labelLower.includes('token');
264
- if (isPassword && el.state?.value) {
265
- el.state.value = '********';
266
- }
267
- }
268
- }
269
- sessionManager.updateActivity(sessionId);
270
- const result = {
271
- page: {
272
- url: tab.page.url(),
273
- title: await tab.page.title(),
274
- type: analysis.pageType,
275
- summary: analysis.summary,
276
- },
277
- elements: filtered,
278
- totalElements,
279
- truncated,
280
- regions,
281
- intents: analysis.intents,
282
- };
283
- // Add stability and dialog info from PageEventTracker
284
- if (tab.events) {
285
- result.stability = tab.events.getStabilityState();
286
- const pending = tab.events.getPendingDialog();
287
- if (pending)
288
- result.pendingDialog = pending;
289
- }
219
+ const result = await toolActions.getPageInfo(toolCtx, sessionId, tab.id, { maxElements, visibleOnly });
290
220
  return textResult(result);
291
221
  }));
292
222
  // ===== get_page_content =====
@@ -296,27 +226,8 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
296
226
  }, safe(async ({ sessionId: rawSessionId, maxLength }) => {
297
227
  const sessionId = await resolveSession(rawSessionId);
298
228
  const tab = getActiveTab(sessionId);
299
- const content = await contentExtractor.extract(tab.page);
300
- // Truncate sections if maxLength specified
301
- if (maxLength && content.sections) {
302
- let totalLen = 0;
303
- const truncatedSections = [];
304
- for (const section of content.sections) {
305
- const sectionText = section.text || '';
306
- if (totalLen + sectionText.length > maxLength) {
307
- const remaining = maxLength - totalLen;
308
- if (remaining > 0) {
309
- truncatedSections.push({ ...section, text: sectionText.slice(0, remaining) });
310
- }
311
- break;
312
- }
313
- truncatedSections.push(section);
314
- totalLen += sectionText.length;
315
- }
316
- content.sections = truncatedSections;
317
- }
318
- sessionManager.updateActivity(sessionId);
319
- return textResult(content);
229
+ const result = await toolActions.getPageContent(toolCtx, sessionId, tab.id, { maxLength });
230
+ return textResult(result);
320
231
  }));
321
232
  // ===== click =====
322
233
  server.tool('click', '点击页面上的元素', {
@@ -381,8 +292,11 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
381
292
  // ===== scroll =====
382
293
  server.tool('scroll', '滚动页面', {
383
294
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
384
- direction: z.enum(['down', 'up']).describe('滚动方向'),
295
+ direction: z.string().describe('滚动方向:down 或 up'),
385
296
  }, safe(async ({ sessionId: rawSessionId, direction }) => {
297
+ if (direction !== 'down' && direction !== 'up') {
298
+ throw invalidParameterError('direction must be one of: down, up');
299
+ }
386
300
  const sessionId = await resolveSession(rawSessionId);
387
301
  const tab = getActiveTab(sessionId);
388
302
  await executeAction(tab.page, 'scroll', undefined, direction);
@@ -431,12 +345,17 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
431
345
  server.tool('press_key', '按下键盘按键,支持组合键(如 Ctrl+A, Shift+Tab)。modifiers 可选: Control, Shift, Alt, Meta', {
432
346
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
433
347
  key: z.string().describe('按键名称,如 Enter, Escape, Tab, ArrowDown, a, c'),
434
- modifiers: z.array(z.enum(['Control', 'Shift', 'Alt', 'Meta'])).optional().describe('修饰键数组,如 ["Control"] 表示 Ctrl+key'),
348
+ modifiers: z.array(z.string()).optional().describe('修饰键数组,如 ["Control"] 表示 Ctrl+key'),
435
349
  }, safe(async ({ sessionId: rawSessionId, key, modifiers }) => {
436
350
  if (modifiers && modifiers.length > 0) {
351
+ for (const mod of modifiers) {
352
+ if (!ALLOWED_MODIFIERS.includes(mod)) {
353
+ throw invalidParameterError(`不允许的修饰键: ${mod}。允许: ${ALLOWED_MODIFIERS.join(', ')}`);
354
+ }
355
+ }
437
356
  // Combo key mode
438
357
  if (!isComboAllowed(modifiers, key)) {
439
- throw new Error(`不允许的组合键: ${modifiers.join('+')}+${key}`);
358
+ throw invalidParameterError(`不允许的组合键: ${modifiers.join('+')}+${key}`);
440
359
  }
441
360
  const sessionId = await resolveSession(rawSessionId);
442
361
  const tab = getActiveTab(sessionId);
@@ -458,7 +377,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
458
377
  }
459
378
  // Single key mode (original logic)
460
379
  if (!ALLOWED_KEYS.has(key)) {
461
- throw new Error(`不允许的按键: ${key}。允许: ${[...ALLOWED_KEYS].join(', ')}`);
380
+ throw invalidParameterError(`不允许的按键: ${key}。允许: ${[...ALLOWED_KEYS].join(', ')}`);
462
381
  }
463
382
  const sessionId = await resolveSession(rawSessionId);
464
383
  const tab = getActiveTab(sessionId);
@@ -471,7 +390,9 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
471
390
  });
472
391
  }));
473
392
  // ===== go_back =====
474
- server.tool('go_back', '返回上一页', { sessionId: z.string().optional().describe('会话ID,不传则使用默认会话') }, safe(async ({ sessionId: rawSessionId }) => {
393
+ server.tool('go_back', '返回上一页', {
394
+ sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
395
+ }, safe(async ({ sessionId: rawSessionId }) => {
475
396
  const sessionId = await resolveSession(rawSessionId);
476
397
  const tab = getActiveTab(sessionId);
477
398
  await executeAction(tab.page, 'back');
@@ -504,12 +425,16 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
504
425
  });
505
426
  }));
506
427
  // ===== wait =====
507
- server.tool('wait', '等待页面加载', {
428
+ server.tool('wait', '按条件等待:time / selector / networkidle / element_hidden', {
508
429
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
509
430
  milliseconds: z.number().optional().describe('等待的毫秒数,默认1000'),
510
431
  selector: z.string().optional().describe('等待指定CSS选择器的元素出现'),
511
- condition: z.enum(['time', 'selector', 'networkidle', 'element_hidden']).optional().describe('等待条件类型'),
432
+ condition: z.string().optional().describe('等待条件类型:time / selector / networkidle / element_hidden'),
512
433
  }, safe(async ({ sessionId: rawSessionId, milliseconds, selector, condition }) => {
434
+ const allowedConditions = ['time', 'selector', 'networkidle', 'element_hidden'];
435
+ if (condition !== undefined && !allowedConditions.includes(condition)) {
436
+ throw invalidParameterError('condition must be one of: time, selector, networkidle, element_hidden');
437
+ }
513
438
  const sessionId = await resolveSession(rawSessionId);
514
439
  const tab = getActiveTab(sessionId);
515
440
  // For condition-based waits, default timeout is 10s; for simple time wait, default is 1s
@@ -518,12 +443,12 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
518
443
  }
519
444
  else if (condition === 'element_hidden') {
520
445
  if (!selector)
521
- throw new Error('selector is required for element_hidden condition');
446
+ throw invalidParameterError('selector is required for element_hidden condition');
522
447
  await tab.page.waitForSelector(selector, { hidden: true, timeout: milliseconds || 10000 });
523
448
  }
524
449
  else if (condition === 'selector' || (!condition && selector)) {
525
450
  if (!selector)
526
- throw new Error('selector is required for selector condition');
451
+ throw invalidParameterError('selector is required for selector condition');
527
452
  await tab.page.waitForSelector(selector, { timeout: milliseconds || 10000 });
528
453
  }
529
454
  else {
@@ -533,10 +458,13 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
533
458
  return textResult({ success: true });
534
459
  }));
535
460
  // ===== execute_javascript =====
536
- server.tool('execute_javascript', '在当前页面执行 JavaScript 脚本。脚本必须通过表达式或 return 返回数据,console.log 的输出不会返回', {
461
+ server.tool('execute_javascript', '在当前页面执行 JavaScript(仅 local 模式可用)。脚本需通过表达式或 return 返回数据,console.log 不会作为返回值', {
537
462
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
538
463
  script: z.string().describe('要执行的 JavaScript 代码'),
539
464
  }, safe(async ({ sessionId: rawSessionId, script }) => {
465
+ if (isRemote) {
466
+ return errorResult('execute_javascript is disabled in remote mode', ErrorCode.INVALID_PARAMETER);
467
+ }
540
468
  const sessionId = await resolveSession(rawSessionId);
541
469
  const tab = getActiveTab(sessionId);
542
470
  let result;
@@ -601,11 +529,8 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
601
529
  tabId: z.string().describe('要关闭的标签页ID'),
602
530
  }, safe(async ({ sessionId: rawSessionId, tabId }) => {
603
531
  const sessionId = await resolveSession(rawSessionId);
604
- const closed = await sessionManager.closeTab(sessionId, tabId);
605
- if (!closed)
606
- throw new Error(`Tab not found: ${tabId}`);
607
- sessionManager.updateActivity(sessionId);
608
- return textResult({ success: true });
532
+ const result = await toolActions.closeTab(toolCtx, sessionId, tabId);
533
+ return textResult(result);
609
534
  }));
610
535
  // ===== switch_tab =====
611
536
  server.tool('switch_tab', '切换到指定标签页', {
@@ -629,38 +554,8 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
629
554
  url: z.string().optional().describe('新标签页要导航到的URL'),
630
555
  }, safe(async ({ sessionId: rawSessionId, url }) => {
631
556
  const sessionId = await resolveSession(rawSessionId);
632
- const tab = await sessionManager.createTab(sessionId);
633
- if (!tab)
634
- throw new Error(`Session not found: ${sessionId}`);
635
- // Auto-switch to the new tab
636
- sessionManager.switchTab(sessionId, tab.id);
637
- let partial = false;
638
- if (url) {
639
- const check = validateUrl(url, options?.urlValidation ?? {});
640
- if (!check.valid) {
641
- const err = new Error(check.reason);
642
- err.errorCode = ErrorCode.INVALID_PARAMETER;
643
- throw err;
644
- }
645
- // Inject saved cookies before navigation (consistent with navigate tool)
646
- await injectCookiesToPage(tab.page);
647
- try {
648
- await tab.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
649
- }
650
- catch (err) {
651
- if (err.name === 'TimeoutError' || err.message?.includes('timeout')) {
652
- partial = true;
653
- }
654
- else {
655
- throw err;
656
- }
657
- }
658
- tab.url = tab.page.url();
659
- // Save cookies after navigation
660
- await saveCookiesFromPage(tab.page);
661
- }
662
- sessionManager.updateActivity(sessionId);
663
- return textResult({ tabId: tab.id, url: tab.page.url(), partial });
557
+ const result = await toolActions.createTab(toolCtx, sessionId, url);
558
+ return textResult(result);
664
559
  }));
665
560
  // ===== list_tabs =====
666
561
  server.tool('list_tabs', '列出当前会话的所有标签页', {
@@ -689,12 +584,15 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
689
584
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
690
585
  fullPage: z.boolean().optional().describe('是否截取整个页面(包括滚动区域),默认 false'),
691
586
  element_id: z.string().optional().describe('截取指定元素的截图(优先于 fullPage)'),
692
- format: z.enum(['png', 'jpeg', 'webp']).optional().describe('截图格式,默认 png'),
587
+ format: z.string().optional().describe('截图格式:png / jpeg / webp,默认 png'),
693
588
  quality: z.number().min(0).max(100).optional().describe('图片质量(仅 jpeg/webp 有效),默认 80'),
694
589
  }, safe(async ({ sessionId: rawSessionId, fullPage, element_id, format, quality }) => {
695
590
  const sessionId = await resolveSession(rawSessionId);
696
591
  const tab = getActiveTab(sessionId);
697
592
  const imgFormat = format || 'png';
593
+ if (!['png', 'jpeg', 'webp'].includes(imgFormat)) {
594
+ throw invalidParameterError('format must be one of: png, jpeg, webp');
595
+ }
698
596
  const mimeType = `image/${imgFormat}`;
699
597
  const screenshotOpts = {
700
598
  encoding: 'base64',
@@ -728,8 +626,9 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
728
626
  const pageTitle = await tab.page.title().catch(() => '');
729
627
  return {
730
628
  content: [
731
- { type: 'text', text: JSON.stringify({ captured: true, url: pageUrl, title: pageTitle, fullPage: !!fullPage, element: element_id || null }) },
629
+ // Some MCP clients only render the first content block; keep image first for compatibility.
732
630
  { type: 'image', data: base64, mimeType },
631
+ { type: 'text', text: JSON.stringify({ captured: true, url: pageUrl, title: pageTitle, fullPage: !!fullPage, element: element_id || null }) },
733
632
  ],
734
633
  };
735
634
  }));
@@ -784,9 +683,12 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
784
683
  // ===== handle_dialog =====
785
684
  server.tool('handle_dialog', '处理页面弹窗(alert/confirm/prompt),接受或拒绝', {
786
685
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
787
- action: z.enum(['accept', 'dismiss']).describe('accept 接受弹窗,dismiss 拒绝弹窗'),
686
+ action: z.string().describe('accept 接受弹窗,dismiss 拒绝弹窗'),
788
687
  text: z.string().optional().describe('为 prompt 弹窗提供输入文本'),
789
688
  }, safe(async ({ sessionId: rawSessionId, action, text }) => {
689
+ if (action !== 'accept' && action !== 'dismiss') {
690
+ throw invalidParameterError('action must be one of: accept, dismiss');
691
+ }
790
692
  const sessionId = await resolveSession(rawSessionId);
791
693
  const tab = getActiveTab(sessionId);
792
694
  if (!tab.events) {
@@ -823,23 +725,20 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
823
725
  }, safe(async ({ sessionId: rawSessionId, timeout, quietMs }) => {
824
726
  const sessionId = await resolveSession(rawSessionId);
825
727
  const tab = getActiveTab(sessionId);
826
- if (!tab.events) {
827
- return textResult({ stable: true, domStable: true, networkPending: 0, loadState: 'loaded' });
828
- }
829
- const maxWait = Math.min(timeout ?? 5000, 30000);
830
- const stable = await tab.events.waitForStable(maxWait, quietMs);
831
- sessionManager.updateActivity(sessionId);
832
- const state = tab.events.getStabilityState();
833
- return textResult({ ...state, stable });
728
+ const result = await toolActions.waitForStable(toolCtx, sessionId, tab.id, { timeout, quietMs });
729
+ return textResult(result);
834
730
  }));
835
731
  // ===== get_network_logs =====
836
732
  server.tool('get_network_logs', '获取页面网络请求日志', {
837
733
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
838
- filter: z.enum(['all', 'xhr', 'failed', 'slow']).optional().describe('过滤类型:all=全部, xhr=仅XHR/Fetch, failed=仅失败, slow=慢请求(>1s)'),
734
+ filter: z.string().optional().describe('过滤类型:all=全部, xhr=仅XHR/Fetch, failed=仅失败, slow=慢请求(>1s)'),
839
735
  maxEntries: z.number().optional().describe('最大返回条数,默认50'),
840
736
  includeHeaders: z.boolean().optional().describe('是否包含请求/响应头,默认false'),
841
737
  urlPattern: z.string().optional().describe('URL 匹配模式(子串匹配)'),
842
738
  }, safe(async ({ sessionId: rawSessionId, filter, maxEntries, includeHeaders, urlPattern }) => {
739
+ if (filter !== undefined && !['all', 'xhr', 'failed', 'slow'].includes(filter)) {
740
+ throw invalidParameterError('filter must be one of: all, xhr, failed, slow');
741
+ }
843
742
  const sessionId = await resolveSession(rawSessionId);
844
743
  const tab = getActiveTab(sessionId);
845
744
  if (!tab.events) {
@@ -877,9 +776,12 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
877
776
  // ===== get_console_logs =====
878
777
  server.tool('get_console_logs', '获取页面控制台日志', {
879
778
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
880
- level: z.enum(['error', 'warn', 'log', 'info', 'debug', 'all']).optional().describe('日志级别过滤,默认只返回 error+warn'),
779
+ level: z.string().optional().describe('日志级别过滤:error / warn / log / info / debug / all,默认只返回 error+warn'),
881
780
  maxEntries: z.number().optional().describe('最大返回条数,默认50'),
882
781
  }, safe(async ({ sessionId: rawSessionId, level, maxEntries }) => {
782
+ if (level !== undefined && !['error', 'warn', 'log', 'info', 'debug', 'all'].includes(level)) {
783
+ throw invalidParameterError('level must be one of: error, warn, log, info, debug, all');
784
+ }
883
785
  const sessionId = await resolveSession(rawSessionId);
884
786
  const tab = getActiveTab(sessionId);
885
787
  if (!tab.events) {
@@ -903,11 +805,14 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
903
805
  return textResult({ logs, truncated });
904
806
  }));
905
807
  // ===== upload_file =====
906
- server.tool('upload_file', '上传文件到 file input 元素', {
808
+ server.tool('upload_file', '上传文件到 file input 元素(仅 local 模式可用)', {
907
809
  sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
908
810
  element_id: z.string().describe('file input 元素的语义ID'),
909
811
  filePath: z.string().describe('要上传的文件路径'),
910
812
  }, safe(async ({ sessionId: rawSessionId, element_id, filePath: rawPath }) => {
813
+ if (isRemote) {
814
+ return errorResult('upload_file is disabled in remote mode', ErrorCode.INVALID_PARAMETER);
815
+ }
911
816
  const sessionId = await resolveSession(rawSessionId);
912
817
  const tab = getActiveTab(sessionId);
913
818
  const resolvedPath = path.resolve(rawPath);
@@ -950,6 +855,8 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
950
855
  sessionManager.updateActivity(sessionId);
951
856
  return textResult({ downloads: tab.events.getDownloads() });
952
857
  }));
858
+ // ===== Task Template Tools (delegated to task-tools.ts) =====
859
+ registerTaskTools(server, toolCtx, runManager, artifactStore, isRemote, safe, resolveSession, createIsolatedSession);
953
860
  return server;
954
861
  }
955
862
  //# sourceMappingURL=browser-mcp-server.js.map