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.
- package/README.md +76 -7
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +2 -1
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/api/mcp-sse.d.ts.map +1 -1
- package/dist/api/mcp-sse.js +26 -8
- package/dist/api/mcp-sse.js.map +1 -1
- package/dist/api/routes.js +1 -1
- package/dist/api/routes.js.map +1 -1
- package/dist/browser/BrowserManager.d.ts.map +1 -1
- package/dist/browser/BrowserManager.js +3 -2
- package/dist/browser/BrowserManager.js.map +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/cli/mcp-stdio.js +1 -1
- package/dist/cli/mcp-stdio.js.map +1 -1
- package/dist/mcp/browser-mcp-server.d.ts +6 -9
- package/dist/mcp/browser-mcp-server.d.ts.map +1 -1
- package/dist/mcp/browser-mcp-server.js +125 -218
- package/dist/mcp/browser-mcp-server.js.map +1 -1
- package/dist/mcp/task-tools.d.ts +6 -0
- package/dist/mcp/task-tools.d.ts.map +1 -0
- package/dist/mcp/task-tools.js +303 -0
- package/dist/mcp/task-tools.js.map +1 -0
- package/dist/task/artifact-store.d.ts +36 -0
- package/dist/task/artifact-store.d.ts.map +1 -0
- package/dist/task/artifact-store.js +115 -0
- package/dist/task/artifact-store.js.map +1 -0
- package/dist/task/cancel-token.d.ts +13 -0
- package/dist/task/cancel-token.d.ts.map +1 -0
- package/dist/task/cancel-token.js +42 -0
- package/dist/task/cancel-token.js.map +1 -0
- package/dist/task/error-codes.d.ts +19 -0
- package/dist/task/error-codes.d.ts.map +1 -0
- package/dist/task/error-codes.js +22 -0
- package/dist/task/error-codes.js.map +1 -0
- package/dist/task/index.d.ts +17 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/index.js +10 -0
- package/dist/task/index.js.map +1 -0
- package/dist/task/run-manager.d.ts +77 -0
- package/dist/task/run-manager.d.ts.map +1 -0
- package/dist/task/run-manager.js +286 -0
- package/dist/task/run-manager.js.map +1 -0
- package/dist/task/run-store.d.ts +39 -0
- package/dist/task/run-store.d.ts.map +1 -0
- package/dist/task/run-store.js +88 -0
- package/dist/task/run-store.js.map +1 -0
- package/dist/task/templates/batch-extract.d.ts +33 -0
- package/dist/task/templates/batch-extract.d.ts.map +1 -0
- package/dist/task/templates/batch-extract.js +153 -0
- package/dist/task/templates/batch-extract.js.map +1 -0
- package/dist/task/templates/login-keep-session.d.ts +34 -0
- package/dist/task/templates/login-keep-session.d.ts.map +1 -0
- package/dist/task/templates/login-keep-session.js +190 -0
- package/dist/task/templates/login-keep-session.js.map +1 -0
- package/dist/task/templates/multi-tab-compare.d.ts +43 -0
- package/dist/task/templates/multi-tab-compare.d.ts.map +1 -0
- package/dist/task/templates/multi-tab-compare.js +204 -0
- package/dist/task/templates/multi-tab-compare.js.map +1 -0
- package/dist/task/templates/registry.d.ts +13 -0
- package/dist/task/templates/registry.d.ts.map +1 -0
- package/dist/task/templates/registry.js +40 -0
- package/dist/task/templates/registry.js.map +1 -0
- package/dist/task/tool-actions.d.ts +114 -0
- package/dist/task/tool-actions.d.ts.map +1 -0
- package/dist/task/tool-actions.js +371 -0
- package/dist/task/tool-actions.js.map +1 -0
- package/dist/task/tool-context.d.ts +26 -0
- package/dist/task/tool-context.d.ts.map +1 -0
- package/dist/task/tool-context.js +2 -0
- package/dist/task/tool-context.js.map +1 -0
- package/dist/utils/url-validator.d.ts +13 -0
- package/dist/utils/url-validator.d.ts.map +1 -1
- package/dist/utils/url-validator.js +64 -0
- package/dist/utils/url-validator.js.map +1 -1
- package/package.json +3 -2
- package/public/agent.html +3 -927
- 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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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', '
|
|
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
|
|
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
|
|
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
|
|
300
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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', '返回上一页', {
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
605
|
-
|
|
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
|
|
633
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
827
|
-
|
|
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.
|
|
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.
|
|
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
|