@mearl/mcp-server 2.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,1837 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mearl MCP Server
4
+ *
5
+ * 提供 MCP 工具让 AI 能够与浏览器交互:
6
+ * - browser_get_requests: 获取历史请求
7
+ * - browser_get_events: 获取 RUM/aplus 埋点事件
8
+ * - browser_get_logs: 获取控制台日志
9
+ * - browser_set_mock: 设置 Mock 数据
10
+ *
11
+ * 命令行参数:
12
+ * - configure / --configure: 运行配置向导
13
+ * - uninstall / --uninstall: 卸载并清理配置
14
+ * - --extension-id=ID: 指定 Chrome Extension ID
15
+ */
16
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
17
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
19
+ import * as client from '@mearl/client';
20
+ import { writeFileSync, appendFileSync, readFileSync, existsSync } from 'fs';
21
+ import { tmpdir } from 'os';
22
+ import { join, dirname } from 'path';
23
+ import { spawn } from 'child_process';
24
+ import { fileURLToPath } from 'url';
25
+ import { RESPONSE_SIZE_SMALL, RESPONSE_SIZE_MEDIUM, RESPONSE_SIZE_LARGE, RESPONSE_SIZE_MAX, } from './constants.js';
26
+ // 日志输出到 stderr(不影响 stdio 通信)
27
+ const LOG_FILE = join(tmpdir(), 'mearl-mcp-server.log');
28
+ const log = (...args) => {
29
+ const msg = `[${new Date().toISOString()}] [MCP Server] ${args.join(' ')}\n`;
30
+ try {
31
+ appendFileSync(LOG_FILE, msg);
32
+ }
33
+ catch {
34
+ // Ignore
35
+ }
36
+ console.error('[MCP Server]', ...args);
37
+ };
38
+ // 解析命令行参数
39
+ function parseArgs() {
40
+ const args = process.argv.slice(2);
41
+ let extensionId;
42
+ let uninstall = false;
43
+ let configure = false;
44
+ for (const arg of args) {
45
+ if (arg.startsWith('--extension-id=')) {
46
+ extensionId = arg.split('=')[1];
47
+ }
48
+ else if (arg.startsWith('--id=')) {
49
+ extensionId = arg.split('=')[1];
50
+ }
51
+ else if (arg === '--uninstall' || arg === 'uninstall') {
52
+ uninstall = true;
53
+ }
54
+ else if (arg === '--configure' || arg === 'configure') {
55
+ configure = true;
56
+ }
57
+ }
58
+ return { extensionId, uninstall, configure };
59
+ }
60
+ /**
61
+ * 分析响应数据,提供统计信息
62
+ */
63
+ function analyzeResponseData(result) {
64
+ const stats = {};
65
+ try {
66
+ if (result && typeof result === 'object' && result.meta) {
67
+ stats.meta = result.meta;
68
+ // 分析实际数据数组
69
+ const dataArray = result.requests || result.logs;
70
+ if (Array.isArray(dataArray) && dataArray.length > 0) {
71
+ const sample = dataArray[0];
72
+ const fieldSizes = [];
73
+ for (const [key, value] of Object.entries(sample)) {
74
+ if (value !== null && value !== undefined) {
75
+ const size = JSON.stringify(value).length;
76
+ fieldSizes.push({ path: key, size });
77
+ }
78
+ }
79
+ // 按大小排序,取前 3 个
80
+ fieldSizes.sort((a, b) => b.size - a.size);
81
+ stats.largestFields = fieldSizes.slice(0, 3);
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // 忽略分析错误
87
+ }
88
+ return stats;
89
+ }
90
+ /**
91
+ * 根据工具名称和参数生成优化建议
92
+ */
93
+ function getOptimizationSuggestions(toolName, args, result, stats) {
94
+ const suggestions = [];
95
+ if (toolName === 'browser_get_requests') {
96
+ const count = args.count || 10;
97
+ const hasFields = args.fields && args.fields.length > 0;
98
+ const includeBody = args.includeBody !== false;
99
+ const keysOnly = args.keysOnly === true;
100
+ // 最优先建议:使用字段提取
101
+ if (!keysOnly && !hasFields && includeBody) {
102
+ suggestions.push('Use "keysOnly: true" to preview data structure first');
103
+ suggestions.push('Use "fields: [...]" to extract only needed fields');
104
+ // 基于实际数据推荐字段
105
+ if (stats.largestFields && stats.largestFields.length > 0) {
106
+ const fieldNames = stats.largestFields
107
+ .filter(f => !['responseBody', 'postData'].includes(f.path))
108
+ .map(f => f.path);
109
+ if (fieldNames.length > 0) {
110
+ suggestions.push(`Example: { fields: ["${fieldNames[0]}", "apiName", "status"] }`);
111
+ }
112
+ }
113
+ }
114
+ // Body 相关建议
115
+ if (includeBody) {
116
+ const hasLargeBody = stats.largestFields?.some(f => ['responseBody', 'postData'].includes(f.path) && f.size > 10000);
117
+ if (hasLargeBody) {
118
+ suggestions.push('Response/request bodies are large - consider setting "includeBody: false"');
119
+ suggestions.push(`Or reduce "maxBodySize" (current: ${args.maxBodySize || 50000} bytes)`);
120
+ }
121
+ }
122
+ // 数量建议
123
+ if (count > 10) {
124
+ suggestions.push(`Reduce "count" from ${count} to fewer requests`);
125
+ }
126
+ // 过滤建议
127
+ if (!args.filter) {
128
+ suggestions.push('Add "filter" regex to match specific APIs (e.g., ".*\\.cart\\..*")');
129
+ }
130
+ // Headers 建议
131
+ if (args.includeHeaders) {
132
+ suggestions.push('Set "includeHeaders: false" to omit headers (often not needed)');
133
+ }
134
+ }
135
+ if (toolName === 'browser_get_logs') {
136
+ const limit = args.limit || 100;
137
+ if (limit > 50) {
138
+ suggestions.push(`Reduce "limit" from ${limit} to fewer logs`);
139
+ }
140
+ if (!args.filter) {
141
+ suggestions.push('Add "filter" to match specific log patterns');
142
+ }
143
+ if (!args.level || args.level === 'all') {
144
+ suggestions.push('Filter by specific "level" (error/warn/info/log)');
145
+ }
146
+ }
147
+ return suggestions;
148
+ }
149
+ /**
150
+ * 智能格式化响应,根据大小决定格式化策略和是否添加提示
151
+ */
152
+ function formatResponseWithSizeCheck(result, toolName, args) {
153
+ // 先序列化为压缩格式测量大小
154
+ const compactStr = JSON.stringify(result);
155
+ const compactSize = Buffer.byteLength(compactStr, 'utf-8');
156
+ // 预估格式化后的大小(约 1.7 倍)
157
+ const estimatedFormattedSize = compactSize * 1.7;
158
+ const sizeKB = Math.round(compactSize / 1024);
159
+ const formattedSizeKB = Math.round(estimatedFormattedSize / 1024);
160
+ // 分析数据特征
161
+ const stats = analyzeResponseData(result);
162
+ // 策略 1: 小响应(<50KB 压缩)- 直接格式化输出,无需前置说明
163
+ // meta 信息已经在 JSON 中,AI 可以直接读取
164
+ if (compactSize < RESPONSE_SIZE_SMALL) {
165
+ return [
166
+ {
167
+ type: 'text',
168
+ text: JSON.stringify(result, null, 2),
169
+ },
170
+ ];
171
+ }
172
+ // 策略 2: 中等响应(50-200KB 压缩)- 格式化 + 简要提示
173
+ if (compactSize < RESPONSE_SIZE_MEDIUM) {
174
+ const suggestions = getOptimizationSuggestions(toolName, args, result, stats);
175
+ let header = `💡 Response size: ${formattedSizeKB}KB (formatted)`;
176
+ if (suggestions.length > 0) {
177
+ header += `\n\nOptimization suggestions:`;
178
+ header += suggestions.map((s, i) => `\n ${i + 1}. ${s}`).join('');
179
+ }
180
+ return [
181
+ {
182
+ type: 'text',
183
+ text: `${header}\n\n${'─'.repeat(60)}\n\n${JSON.stringify(result, null, 2)}`,
184
+ },
185
+ ];
186
+ }
187
+ // 策略 3: 大响应(200-500KB 压缩)- 压缩格式 + 优化提示
188
+ if (compactSize < RESPONSE_SIZE_LARGE) {
189
+ const suggestions = getOptimizationSuggestions(toolName, args, result, stats);
190
+ let header = `⚠️ Large response: ${sizeKB}KB (compact format to save space)\n`;
191
+ header += `Formatted size would be ~${formattedSizeKB}KB\n`;
192
+ // 如果有 meta,提取关键信息到头部
193
+ if (stats.meta) {
194
+ header += `\n📊 Data: ${stats.meta.count} items returned`;
195
+ if (stats.meta.totalMatched > stats.meta.count) {
196
+ header += ` (${stats.meta.totalMatched} matched, ${stats.meta.totalInBuffer} in buffer)`;
197
+ }
198
+ }
199
+ if (stats.largestFields && stats.largestFields.length > 0) {
200
+ header += `\nLargest fields per item:`;
201
+ stats.largestFields.forEach(f => {
202
+ header += `\n • ${f.path}: ~${Math.round(f.size / 1024)}KB`;
203
+ });
204
+ }
205
+ if (suggestions.length > 0) {
206
+ header += `\n\n🎯 Optimization suggestions:`;
207
+ header += suggestions.map((s, i) => `\n ${i + 1}. ${s}`).join('');
208
+ }
209
+ return [
210
+ {
211
+ type: 'text',
212
+ text: `${header}\n\n${'─'.repeat(60)}\n\n${compactStr}`,
213
+ },
214
+ ];
215
+ }
216
+ // 策略 4: 超大响应(500KB-1MB 压缩)- 压缩格式 + 详细建议
217
+ if (compactSize < RESPONSE_SIZE_MAX) {
218
+ const suggestions = getOptimizationSuggestions(toolName, args, result, stats);
219
+ let header = `🚨 Very large response: ${sizeKB}KB\n`;
220
+ header += `This may cause performance issues and exceed context limits.\n`;
221
+ if (stats.meta) {
222
+ header += `\n📊 Data: ${stats.meta.count} items returned`;
223
+ if (stats.meta.totalMatched > stats.meta.count) {
224
+ header += ` (${stats.meta.totalMatched} matched, ${stats.meta.totalInBuffer} in buffer)`;
225
+ }
226
+ }
227
+ if (stats.largestFields && stats.largestFields.length > 0) {
228
+ header += `\nLargest fields per item:`;
229
+ stats.largestFields.forEach(f => {
230
+ header += `\n • ${f.path}: ~${Math.round(f.size / 1024)}KB`;
231
+ });
232
+ }
233
+ header += `\n\n🎯 STRONGLY RECOMMENDED optimizations:`;
234
+ if (suggestions.length > 0) {
235
+ header += suggestions.map((s, i) => `\n ${i + 1}. ${s}`).join('');
236
+ }
237
+ else {
238
+ header += `\n 1. Reduce the amount of data requested`;
239
+ header += `\n 2. Use filtering parameters to narrow results`;
240
+ }
241
+ return [
242
+ {
243
+ type: 'text',
244
+ text: `${header}\n\n${'─'.repeat(60)}\n\n${compactStr}`,
245
+ },
246
+ ];
247
+ }
248
+ // 策略 5: 超超大响应(>1MB 压缩)- 截断 + 错误级别建议
249
+ const truncateSize = RESPONSE_SIZE_MAX;
250
+ const truncatedStr = compactStr.substring(0, truncateSize);
251
+ const suggestions = getOptimizationSuggestions(toolName, args, result, stats);
252
+ let header = `🛑 Response too large: ${sizeKB}KB (TRUNCATED to ${Math.round(truncateSize / 1024)}KB)\n`;
253
+ header += `Original size: ${sizeKB}KB, showing only ${Math.round(truncateSize / 1024)}KB\n`;
254
+ header += `\n⚠️ This response is unusable at this size. You MUST optimize the request.\n`;
255
+ if (stats.meta) {
256
+ header += `\n📊 Data: ${stats.meta.count} items (${stats.meta.totalMatched} matched, ${stats.meta.totalInBuffer} in buffer)`;
257
+ }
258
+ if (stats.largestFields && stats.largestFields.length > 0) {
259
+ header += `\nLargest fields per item:`;
260
+ stats.largestFields.forEach(f => {
261
+ header += `\n • ${f.path}: ~${Math.round(f.size / 1024)}KB (${Math.round((f.size / compactSize) * 100)}% of total)`;
262
+ });
263
+ }
264
+ header += `\n\n🎯 REQUIRED actions:`;
265
+ if (suggestions.length > 0) {
266
+ header += suggestions.map((s, i) => `\n ${i + 1}. ${s}`).join('');
267
+ }
268
+ else {
269
+ header += `\n 1. Significantly reduce the data requested`;
270
+ header += `\n 2. Use "fields" to extract only essential data`;
271
+ header += `\n 3. Add filters to narrow down results`;
272
+ }
273
+ return [
274
+ {
275
+ type: 'text',
276
+ text: `${header}\n\n${'─'.repeat(60)}\n\n${truncatedStr}\n\n... <TRUNCATED ${sizeKB - Math.round(truncateSize / 1024)}KB>`,
277
+ },
278
+ ];
279
+ }
280
+ async function main() {
281
+ // 解析命令行参数
282
+ const { extensionId: _extensionId, uninstall, configure } = parseArgs();
283
+ // 如果是配置命令,执行配置脚本并退出
284
+ if (configure) {
285
+ const __filename = fileURLToPath(import.meta.url);
286
+ const __dirname = dirname(__filename);
287
+ const configureScript = join(__dirname, 'configure.js');
288
+ // 透传 configure 子命令参数(例如 --extension-id / --skip-mcp / --get)
289
+ const configureArgs = process.argv
290
+ .slice(2)
291
+ .filter(arg => arg !== 'configure' && arg !== '--configure');
292
+ const child = spawn('node', [configureScript, ...configureArgs], {
293
+ stdio: 'inherit',
294
+ });
295
+ child.on('exit', code => {
296
+ process.exit(code || 0);
297
+ });
298
+ return;
299
+ }
300
+ // 如果是卸载命令,执行卸载脚本并退出
301
+ if (uninstall) {
302
+ const __filename = fileURLToPath(import.meta.url);
303
+ const __dirname = dirname(__filename);
304
+ const uninstallScript = join(__dirname, 'uninstall.js');
305
+ const child = spawn('node', [uninstallScript], {
306
+ stdio: 'inherit',
307
+ });
308
+ child.on('exit', code => {
309
+ process.exit(code || 0);
310
+ });
311
+ return;
312
+ }
313
+ // 初始化日志文件
314
+ writeFileSync(LOG_FILE, `=== Mearl MCP Server Started at ${new Date().toISOString()} ===\n`);
315
+ log('Starting Mearl MCP Server...');
316
+ log('MCP Server initialized, will connect to Chrome Extension when tools are called');
317
+ // 创建 MCP Server
318
+ const server = new Server({
319
+ name: 'mearl',
320
+ version: '1.0.0',
321
+ }, {
322
+ capabilities: {
323
+ tools: {},
324
+ },
325
+ instructions: '浏览器交互默认工作流:首次进入页面时使用 browser_page_snapshot 建立页面基线;后续点击、输入、悬停、滚动、按键和上传直接使用 browser_page_click/type/hover/scroll/press/upload,它们默认内置观察,动作与异步稳定判断在一次调用内完成,返回 { action, observation };传 observe:false 可关闭观察仅执行裸动作。观察结果不替代完整页面理解:observation.mode 为 delta 时确认 scope="main-document",再读取 effects.notifications、effects.interactives 和 effects.focus;notifications 中 change=transient 表示已经消失的 Toast、状态或错误提示。为 navigation 时先检查 ready,ready=true 表示文档、相关业务请求和可见骨架状态已经稳定,可在新页面重建 snapshot。其他情况下仅当 fullSnapshotRecommended 为 true 时根据 snapshotReasons 回退,并选择最小范围:已知区域用 rootSelector,长列表用 mode="viewport",只需控件用 mode="interactive";缺少 AX 控件语义时 interactive 会在同一次调用中回退到 viewport,并返回 fallbackMode。视觉任务再使用 screenshot。不要在每次动作后固定调用 browser_page_wait、snapshot 或 screenshot。',
326
+ });
327
+ // 页面交互工具共享的 observe 参数 schema:默认开启内置观察,传 false 关闭
328
+ const OBSERVE_INPUT_SCHEMA = {
329
+ type: ['object', 'boolean'],
330
+ description: '观察选项。默认开启内置观察(等待相关 DOM、XHR/Fetch/MTop 请求或导航稳定后返回页面变化信号);传 false 关闭观察,仅执行裸动作并直接返回其结果',
331
+ properties: {
332
+ quietMs: {
333
+ type: 'number',
334
+ description: '最后一次 DOM 变化后等待的稳定窗口,默认 100ms',
335
+ },
336
+ firstChangeTimeoutMs: {
337
+ type: 'number',
338
+ description: '没有发生 DOM 变化时最多等待多久,默认 500ms',
339
+ },
340
+ timeoutMs: {
341
+ type: 'number',
342
+ description: '整个观察阶段的最大时间,默认 3000ms',
343
+ },
344
+ navigationGraceMs: {
345
+ type: 'number',
346
+ description: '低信息量点击或按键后等待导航开始的基础宽限时间,默认 1000ms;若仍有网络活动会自动延长,有明确 effects 时不等待',
347
+ },
348
+ navigationTimeoutMs: {
349
+ type: 'number',
350
+ description: '导航开始后等待页面就绪的最大时间,默认 15000ms',
351
+ },
352
+ networkIdleMs: {
353
+ type: 'number',
354
+ description: '文档加载完成且相关 XHR/Fetch/MTop 请求归零后继续保持静默的时间,默认 500ms,用于跳过业务骨架屏',
355
+ },
356
+ contentReadyAfterMs: {
357
+ type: 'number',
358
+ description: '动作开始后允许用结构稳定兜底的保护期,默认 5000ms,并自动限制在导航超时之前;保护期内仍优先等待网络静默',
359
+ },
360
+ },
361
+ };
362
+ // 定义工具列表
363
+ const tools = [
364
+ {
365
+ name: 'browser_get_requests',
366
+ description: '获取浏览器中最近的 API 请求历史记录,包含完整的请求和响应信息。适用于分析性能、排查问题、查看数据流等场景。无需等待用户操作,立即返回已有数据。',
367
+ inputSchema: {
368
+ type: 'object',
369
+ properties: {
370
+ count: {
371
+ type: 'number',
372
+ description: '获取的请求数量,默认 10 个。最多可获取最近 1000 个请求',
373
+ default: 10,
374
+ },
375
+ filter: {
376
+ type: 'string',
377
+ description: '过滤 API 名称的正则表达式(可选)。例如: ".*\\.cart\\..*" 只返回购物车相关接口',
378
+ },
379
+ since: {
380
+ type: 'number',
381
+ description: '从多久之前开始获取(秒)。例如: 60 表示获取最近 1 分钟内的请求',
382
+ },
383
+ includeBody: {
384
+ type: 'boolean',
385
+ description: '是否包含请求体和响应体。false 时将省略 postData 和 responseBody 字段以减少数据量。默认 true',
386
+ default: true,
387
+ },
388
+ includeHeaders: {
389
+ type: 'boolean',
390
+ description: '是否包含请求头和响应头。false 时将省略 headers 和 responseHeaders 字段以减少数据量。默认 false',
391
+ default: false,
392
+ },
393
+ maxBodySize: {
394
+ type: 'number',
395
+ description: '单个请求/响应体的最大字节数。超过此大小将被截断。默认 50000(约 50KB)',
396
+ default: 50000,
397
+ },
398
+ fields: {
399
+ type: 'array',
400
+ items: {
401
+ type: 'string',
402
+ },
403
+ description: '从响应体中提取指定字段,支持点号路径。例如: ["data.result.list", "data.total"] 只返回这些字段,大幅减少数据量',
404
+ },
405
+ keysOnly: {
406
+ type: 'boolean',
407
+ description: '只返回数据结构(字段路径和类型),不返回实际值。设为 true 可先预览数据结构,再决定提取哪些字段。默认 false',
408
+ default: false,
409
+ },
410
+ source: {
411
+ type: 'string',
412
+ enum: ['mtop', 'requests'],
413
+ description: '数据源。mtop: mtop 接口(默认); requests: 普通 XHR/Fetch 请求。不传则使用 DevTools panel 当前所处模式',
414
+ },
415
+ },
416
+ },
417
+ },
418
+ {
419
+ name: 'browser_get_events',
420
+ description: '获取浏览器中最近的 RUM/aplus 埋点事件,包含页面资源性能指标、用户行为埋点、SPM 点击事件等。',
421
+ inputSchema: {
422
+ type: 'object',
423
+ properties: {
424
+ source: {
425
+ type: 'string',
426
+ enum: ['rum', 'aplus', 'all'],
427
+ description: '事件来源。rum: RUM 性能监控事件; aplus: aplus 埋点事件; all: 全部(默认)',
428
+ default: 'all',
429
+ },
430
+ event_type: {
431
+ type: 'string',
432
+ description: '事件类型正则过滤。例如: "rum/view" 匹配页面 view 事件, "aplus/CLK" 匹配点击埋点',
433
+ },
434
+ filter: {
435
+ type: 'string',
436
+ description: '正则过滤,匹配事件的 name 或 url 字段',
437
+ },
438
+ since: {
439
+ type: 'number',
440
+ description: '从多久之前开始获取(秒)。例如: 60 表示获取最近 1 分钟内的事件',
441
+ },
442
+ limit: {
443
+ type: 'number',
444
+ description: '返回条数上限,默认 20',
445
+ default: 20,
446
+ },
447
+ },
448
+ },
449
+ },
450
+ {
451
+ name: 'browser_get_logs',
452
+ description: '获取浏览器控制台日志(console.log/warn/error/info)。适用于排查前端错误、调试代码执行流程、查看调试信息等场景。',
453
+ inputSchema: {
454
+ type: 'object',
455
+ properties: {
456
+ level: {
457
+ type: 'string',
458
+ enum: ['log', 'warn', 'error', 'info', 'all'],
459
+ description: '日志级别过滤。log: 普通日志, warn: 警告, error: 错误, info: 信息, all: 所有级别。默认 all',
460
+ default: 'all',
461
+ },
462
+ since: {
463
+ type: 'number',
464
+ description: '从多久之前开始获取日志(秒)。例如: 60 表示获取最近 1 分钟内的日志',
465
+ },
466
+ limit: {
467
+ type: 'number',
468
+ description: '最大返回日志数量,默认 100。避免返回过多数据',
469
+ default: 100,
470
+ },
471
+ filter: {
472
+ type: 'string',
473
+ description: '过滤日志内容的正则表达式或字符串(可选)。支持正则表达式模式(不区分大小写),若正则无效则回退到字符串包含匹配。例如: "error|warning" 匹配包含 error 或 warning 的日志',
474
+ },
475
+ },
476
+ },
477
+ },
478
+ {
479
+ name: 'browser_set_mock',
480
+ description: '动态设置 API 接口的 Mock 数据,用于测试不同数据场景。修改后,浏览器中对该接口的所有请求都会返回 Mock 数据。适用于前端开发测试、边界情况验证、UI 状态测试等。',
481
+ inputSchema: {
482
+ type: 'object',
483
+ properties: {
484
+ apiName: {
485
+ type: 'string',
486
+ description: 'API 名称或正则表达式模式。例如: "mtop.cart.query" 精确匹配, ".*\\.cart\\..*" 匹配所有购物车接口',
487
+ },
488
+ mockData: {
489
+ type: ['object', 'string'],
490
+ description: '【方式一】直接设置完整 Mock 响应数据(JSON 对象)。传入后该接口将完全返回此数据,不再请求真实服务端。示例: {"data":{"result":{"list":[]}},"ret":["SUCCESS"]}。注意:mockData 和 fields 二选一,同时提供时优先使用 fields。大体积响应体可传文件引用字符串 "@/abs/resp.json"(从该文件读取,建议绝对路径)以减少上下文占用',
491
+ },
492
+ fields: {
493
+ type: 'array',
494
+ description: '【方式二】基于真实响应修改指定字段。系统会先获取该接口的原始响应数据,然后按指定路径修改字段值。适合在真实数据基础上做微调',
495
+ items: {
496
+ type: 'object',
497
+ properties: {
498
+ path: {
499
+ type: 'string',
500
+ description: '字段路径,支持点号和数组索引。例如: "data.result.passengerList[0].name.text"',
501
+ },
502
+ value: {
503
+ description: '要设置的值,可以是任意类型(字符串、数字、对象、数组等)',
504
+ },
505
+ },
506
+ required: ['path', 'value'],
507
+ },
508
+ },
509
+ enabled: {
510
+ type: 'boolean',
511
+ description: '是否启用 Mock。true: 启用 Mock, false: 禁用 Mock 恢复真实数据。默认 true',
512
+ default: true,
513
+ },
514
+ duration: {
515
+ type: 'number',
516
+ description: '完整响应时长(毫秒)。可选,mock 生效时的总耗时,绕过真实服务器精确控制(用于测试加载状态、超时等场景)',
517
+ },
518
+ conditions: {
519
+ type: 'object',
520
+ additionalProperties: true,
521
+ description: '请求 query/body 字段的子集匹配规则;保留字段 $index 从 0 开始指定同一场景的调用顺序。相同 $index 再次设置会原地覆盖,超过最后一项后持续使用最后一项',
522
+ },
523
+ payloadFile: {
524
+ type: 'string',
525
+ description: '从文件读取 mock 数据(JSON 格式)。当 mockData 很大时,可将完整 JSON 写入文件,通过此参数指定文件路径。文件内容会被解析为 mockData。注意:此参数与 mockData/fields 互斥,优先级最高',
526
+ },
527
+ },
528
+ required: ['apiName'],
529
+ },
530
+ },
531
+ {
532
+ name: 'browser_get_mocks',
533
+ description: '查看当前所有已设置的 Mock 配置,包括接口名称、是否启用、以及 Mock 数据内容。用于排查 Mock 是否生效、Mock 数据是否正确等问题。',
534
+ inputSchema: {
535
+ type: 'object',
536
+ properties: {
537
+ apiName: {
538
+ type: 'string',
539
+ description: '过滤指定接口,支持正则表达式。例如: "mtop.cart.query" 或 ".*\\.cart\\..*"。不传则返回所有 Mock',
540
+ },
541
+ },
542
+ },
543
+ },
544
+ {
545
+ name: 'browser_send_request',
546
+ description: '通过浏览器发起 HTTP 代理请求,自动携带目标域名的 Cookie(使用当前浏览器登录态)。支持 GET/POST/PUT/DELETE/PATCH 等方法,适用于需要用户登录态的接口调试、数据爬取、接口测试等场景。',
547
+ inputSchema: {
548
+ type: 'object',
549
+ properties: {
550
+ url: {
551
+ type: 'string',
552
+ description: '目标请求 URL,必须是完整 URL(含 http:// 或 https://)',
553
+ },
554
+ method: {
555
+ type: 'string',
556
+ enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
557
+ description: 'HTTP 请求方法,默认 GET',
558
+ default: 'GET',
559
+ },
560
+ headers: {
561
+ type: 'object',
562
+ description: '自定义请求头(键值对),会与自动生成的 Cookie 头合并',
563
+ additionalProperties: { type: 'string' },
564
+ },
565
+ params: {
566
+ type: 'object',
567
+ description: 'URL 查询参数(键值对),会追加到 URL 中',
568
+ additionalProperties: { type: 'string' },
569
+ },
570
+ body: {
571
+ description: '请求体。传入对象时自动序列化为 JSON 并设置 Content-Type: application/json;传入字符串时原样发送',
572
+ },
573
+ withCookies: {
574
+ type: 'boolean',
575
+ description: '是否自动携带浏览器中该域名的 Cookie,默认 true',
576
+ default: true,
577
+ },
578
+ cookieDomain: {
579
+ type: 'string',
580
+ description: '指定获取 Cookie 的域名(默认从 url 中自动提取域名)',
581
+ },
582
+ responseType: {
583
+ type: 'string',
584
+ enum: ['json', 'text', 'auto'],
585
+ description: '响应格式。auto: 自动检测(默认); json: 强制解析为 JSON; text: 返回原始文本。注意:auto 模式下若 Content-Type 为图片/音视频/PDF/压缩包等二进制类型,body 将以 base64 字符串返回,bodyType 为 "base64"',
586
+ default: 'auto',
587
+ },
588
+ timeout: {
589
+ type: 'number',
590
+ description: '请求超时时间(毫秒),默认 30000(30 秒)',
591
+ default: 30000,
592
+ },
593
+ },
594
+ required: ['url'],
595
+ },
596
+ },
597
+ {
598
+ name: 'browser_send_mtop_request',
599
+ description: '在当前浏览器页面上下文中发起 mtop API 请求。自动处理签名计算(md5)、Cookie 中的 token 提取、时间戳生成等,无需手动构造签名。适用于:调试 mtop 接口、验证接口返回数据、测试不同参数组合等场景。注意:需要当前页面已登录(有对应的 Cookie),且 DevTools 面板已打开。',
600
+ inputSchema: {
601
+ type: 'object',
602
+ properties: {
603
+ api: {
604
+ type: 'string',
605
+ description: 'mtop API 名称,例如: "mtop.trade.order.create"、"mtop.fliggy.flyrs.render"。以 "mopen." 开头的会使用 mopen 签名算法',
606
+ },
607
+ data: {
608
+ description: '请求数据(JSON 对象),即 mtop 请求中的 data 参数。例如: {"itemId": "12345"}',
609
+ },
610
+ version: {
611
+ type: 'string',
612
+ description: 'API 版本号,默认 "1.0"',
613
+ default: '1.0',
614
+ },
615
+ method: {
616
+ type: 'string',
617
+ enum: ['GET', 'POST'],
618
+ description: 'HTTP 请求方法,默认 GET。POST 时 data 参数放在请求体中',
619
+ default: 'GET',
620
+ },
621
+ appKey: {
622
+ type: 'string',
623
+ description: 'appKey,默认 "12574478"',
624
+ },
625
+ env: {
626
+ type: 'string',
627
+ enum: ['online', 'pre'],
628
+ description: '请求环境。online: 线上(默认,h5api.m.taobao.com); pre: 预发(h5api.wapa.taobao.com)',
629
+ default: 'online',
630
+ },
631
+ timeout: {
632
+ type: 'number',
633
+ description: '请求超时时间(毫秒),默认 10000(10 秒)',
634
+ default: 10000,
635
+ },
636
+ },
637
+ required: ['api'],
638
+ },
639
+ },
640
+ {
641
+ name: 'browser_get_api_schema',
642
+ description: '获取 MTOP API 的请求参数 JSON Schema,用于生成接口调用代码。返回接口的完整参数定义,包括参数类型、是否必需、默认值等信息。',
643
+ inputSchema: {
644
+ type: 'object',
645
+ properties: {
646
+ api: {
647
+ type: 'string',
648
+ description: 'API 名称,例如: "mtop.fliggy.flyrs.render"',
649
+ },
650
+ version: {
651
+ type: 'string',
652
+ description: 'API 版本号,默认 "1.0"',
653
+ default: '1.0',
654
+ },
655
+ fields: {
656
+ type: 'array',
657
+ items: { type: 'string', enum: ['schema', 'hsf'] },
658
+ description: '指定返回哪些字段。schema: 请求参数 JSON Schema;hsf: HSF 接口信息(服务名、方法、版本)。不传则返回全部。',
659
+ },
660
+ },
661
+ required: ['api'],
662
+ },
663
+ },
664
+ {
665
+ name: 'browser_set_rule',
666
+ description: '添加 Chrome declarativeNetRequest 规则,用于拦截、重定向或修改请求。规则持久化保存,浏览器重启后仍生效。支持:重定向请求到另一地址、修改请求/响应头、阻止请求、升级到 HTTPS 等。适用于:本地开发时代理到本地服务、修改请求头调试、拦截特定请求等。',
667
+ inputSchema: {
668
+ type: 'object',
669
+ properties: {
670
+ actionType: {
671
+ type: 'string',
672
+ enum: ['redirect', 'block', 'allow', 'upgradeScheme', 'modifyHeaders'],
673
+ description: '规则类型。redirect: 重定向请求到另一地址; block: 阻止请求; allow: 允许请求; upgradeScheme: 升级到 HTTPS; modifyHeaders: 修改请求/响应头',
674
+ },
675
+ filter: {
676
+ type: 'string',
677
+ description: 'URL 匹配规则(正则表达式)。例如: "^https://h5api\\.m\\.taobao\\.com/.*" 匹配所有淘宝 h5api 请求',
678
+ },
679
+ redirectUrl: {
680
+ type: 'string',
681
+ description: '【redirect 类型必填】重定向目标 URL。支持正则替换占位符 \\1 \\2 等(引用 filter 中的捕获组)',
682
+ },
683
+ requestHeaders: {
684
+ type: 'array',
685
+ description: '【modifyHeaders 类型】要修改的请求头列表',
686
+ items: {
687
+ type: 'object',
688
+ properties: {
689
+ header: { type: 'string', description: '请求头名称,如 "x-custom-header"' },
690
+ operation: {
691
+ type: 'string',
692
+ enum: ['set', 'append', 'remove'],
693
+ description: 'set: 设置/覆盖; append: 追加; remove: 删除',
694
+ },
695
+ value: { type: 'string', description: '请求头值(remove 时可省略)' },
696
+ },
697
+ required: ['header', 'operation'],
698
+ },
699
+ },
700
+ responseHeaders: {
701
+ type: 'array',
702
+ description: '【modifyHeaders 类型】要修改的响应头列表',
703
+ items: {
704
+ type: 'object',
705
+ properties: {
706
+ header: { type: 'string', description: '响应头名称' },
707
+ operation: {
708
+ type: 'string',
709
+ enum: ['set', 'append', 'remove'],
710
+ description: 'set: 设置/覆盖; append: 追加; remove: 删除',
711
+ },
712
+ value: { type: 'string', description: '响应头值(remove 时可省略)' },
713
+ },
714
+ required: ['header', 'operation'],
715
+ },
716
+ },
717
+ initiatorDomains: {
718
+ type: 'string',
719
+ description: '发起请求的域名限制(逗号分隔)。只有从这些域名发起的请求才会匹配此规则。例如: "taobao.com,tmall.com"',
720
+ },
721
+ resourceTypes: {
722
+ type: 'array',
723
+ description: '资源类型限制。不填则匹配所有类型',
724
+ items: {
725
+ type: 'string',
726
+ enum: [
727
+ 'main_frame',
728
+ 'sub_frame',
729
+ 'script',
730
+ 'stylesheet',
731
+ 'image',
732
+ 'xmlhttprequest',
733
+ 'websocket',
734
+ 'other',
735
+ ],
736
+ },
737
+ },
738
+ priority: {
739
+ type: 'number',
740
+ description: '规则优先级,数字越大优先级越高。多个规则匹配同一请求时,优先级高的生效。默认 1',
741
+ default: 1,
742
+ },
743
+ description: {
744
+ type: 'string',
745
+ description: '规则描述,便于识别规则用途',
746
+ },
747
+ },
748
+ required: ['actionType', 'filter'],
749
+ },
750
+ },
751
+ {
752
+ name: 'browser_get_rules',
753
+ description: '查看当前存储的全部请求规则(含用户在插件 Options 面板手动配置的)。返回规则总开关状态和规则列表,字段结构与 browser_set_rule 入参对齐,用于确认规则是否生效或排查请求被意外重定向/修改的问题。',
754
+ inputSchema: {
755
+ type: 'object',
756
+ properties: {},
757
+ },
758
+ },
759
+ {
760
+ name: 'browser_page_screenshot',
761
+ description: '获取当前浏览器活动标签页的视觉截图,以 Base64 编码返回。支持全页截图或指定 CSS 选择器截取单个元素(类似 DevTools "Capture node screenshot")。适用于视觉判断、Canvas、跨域 iframe、UI 排查和保存快照;常规页面动作后优先读取其内置观察的增量结果,不要固定在每次动作后截图。',
762
+ inputSchema: {
763
+ type: 'object',
764
+ properties: {
765
+ tabId: {
766
+ type: 'number',
767
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
768
+ },
769
+ selector: {
770
+ type: 'string',
771
+ description: 'CSS 选择器。提供后只截取该元素区域(会自动滚动元素到可见区域),不提供则截取整个页面',
772
+ },
773
+ format: {
774
+ type: 'string',
775
+ enum: ['png', 'jpeg'],
776
+ description: '截图格式。png: PNG 格式(无损,默认); jpeg: JPEG 格式(有损压缩,体积更小)',
777
+ default: 'png',
778
+ },
779
+ quality: {
780
+ type: 'number',
781
+ description: 'JPEG 质量(仅 format 为 jpeg 时有效)。范围 0-100,默认 80。数值越高质量越好但文件越大',
782
+ default: 80,
783
+ },
784
+ },
785
+ required: ['tabId'],
786
+ },
787
+ },
788
+ {
789
+ name: 'browser_page_selected_element',
790
+ description: '获取 Chrome DevTools Elements 面板中当前选中元素的详细信息,包含布局尺寸、CSS 计算样式、属性、DOM 结构等。适用于:分析布局问题、排查样式异常、了解元素几何信息、辅助 UI 优化等场景。使用前需要在 Elements 面板中选中目标元素(点击面板元素或使用 inspect 工具点击页面元素)。',
791
+ inputSchema: {
792
+ type: 'object',
793
+ properties: {
794
+ tabId: {
795
+ type: 'number',
796
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
797
+ },
798
+ includeOuterHTML: {
799
+ type: 'boolean',
800
+ description: '是否包含元素的 outerHTML(最多 3000 字符)。设为 false 可减少数据量。默认 true',
801
+ default: true,
802
+ },
803
+ includeScreenshot: {
804
+ type: 'boolean',
805
+ description: '是否返回选中元素截图。开启后会自动滚动到元素并按默认策略截图(PNG、8px 边距),返回 image 类型内容。默认 false',
806
+ default: false,
807
+ },
808
+ },
809
+ required: ['tabId'],
810
+ },
811
+ },
812
+ {
813
+ name: 'get_user_info',
814
+ description: '获取当前登录用户的信息。默认只返回工号(userId),设置 detail 为 true 时返回完整的用户信息(姓名、花名、部门、BU 等)。',
815
+ inputSchema: {
816
+ type: 'object',
817
+ properties: {
818
+ detail: {
819
+ type: 'boolean',
820
+ description: '是否获取完整用户信息。默认 false(只返回工号);设置为 true 时返回姓名、花名、部门、BU 等详细信息',
821
+ default: false,
822
+ },
823
+ },
824
+ required: [],
825
+ },
826
+ },
827
+ {
828
+ name: 'tdbank_account',
829
+ description: '通过 TDBank SSO 将当前浏览器登录为指定测试账号,供 taobao.com 及其子域页面使用。一个工具完成获取当前账号、账号列表、切换账号、快速借用并切换账号;使用前需要浏览器已登录 tdbank.alibaba-inc.com。',
830
+ inputSchema: {
831
+ type: 'object',
832
+ properties: {
833
+ operation: {
834
+ type: 'string',
835
+ enum: ['current', 'list', 'switch', 'borrow'],
836
+ description: '操作类型。current=当前登录账号,list=账号列表,switch=通过 TDBank SSO 切换 taobao.com 系页面账号,borrow=快速借用并登录该账号',
837
+ default: 'list',
838
+ },
839
+ refresh: {
840
+ type: 'boolean',
841
+ description: 'list 时是否强制刷新账号列表,默认 false',
842
+ default: false,
843
+ },
844
+ pageIndex: {
845
+ type: 'number',
846
+ description: 'list 时的页码,默认 1',
847
+ default: 1,
848
+ },
849
+ accountId: {
850
+ type: ['string', 'number'],
851
+ description: 'switch 时使用账号列表中的 id',
852
+ },
853
+ havanaId: {
854
+ type: ['string', 'number'],
855
+ description: 'switch 时也可直接指定 havanaId;此时 site 必填',
856
+ },
857
+ site: {
858
+ type: ['string', 'number'],
859
+ description: '直接指定 havanaId 时必填,账号列表中的 site 也会自动使用',
860
+ },
861
+ query: {
862
+ type: 'string',
863
+ description: 'borrow 时输入要借用的账号 loginId 或关键词',
864
+ },
865
+ tabId: {
866
+ type: 'number',
867
+ description: 'CDP 模式下可指定用于读取 Cookie 的标签页 ID',
868
+ },
869
+ },
870
+ },
871
+ },
872
+ {
873
+ name: 'browser_launch',
874
+ description: '启动一个拥有独立 Profile 的托管 Chrome 浏览器。默认 headless,可复用当前控制浏览器的 TDBank 登录态,通过 SSO 为新浏览器登录指定测试账号,供 taobao.com 及其子域页面使用;多个实例之间 Cookie 和存储完全隔离。',
875
+ inputSchema: {
876
+ type: 'object',
877
+ properties: {
878
+ name: {
879
+ type: 'string',
880
+ description: '实例名称,后续可用 browser_select_browser 按名称或返回的 browserId 选择',
881
+ },
882
+ headless: {
883
+ type: 'boolean',
884
+ description: '是否使用 headless=new,默认 true',
885
+ default: true,
886
+ },
887
+ userAgentMode: {
888
+ type: 'string',
889
+ enum: ['default', 'desktop'],
890
+ description: 'UA 模式:default 使用 Chrome 默认 UA,desktop 使用匹配本机 Chrome 版本和宿主平台的桌面 UA',
891
+ default: 'default',
892
+ },
893
+ persistent: {
894
+ type: 'boolean',
895
+ description: '关闭后是否保留独立 Profile,默认 false;false 时自动清理',
896
+ default: false,
897
+ },
898
+ url: {
899
+ type: 'string',
900
+ description: 'TDBank SSO 登录完成后在新浏览器打开的初始 URL,通常为 taobao.com 系页面',
901
+ },
902
+ executablePath: {
903
+ type: 'string',
904
+ description: 'Chrome 可执行文件路径;省略时自动发现,也可用环境变量配置',
905
+ },
906
+ accountId: {
907
+ type: ['string', 'number'],
908
+ description: 'TDBank 账号列表中的 accountId',
909
+ },
910
+ havanaId: {
911
+ type: ['string', 'number'],
912
+ description: '直接指定 havanaId;同时必须提供 site',
913
+ },
914
+ site: {
915
+ type: ['string', 'number'],
916
+ description: 'havanaId 对应的站点',
917
+ },
918
+ query: {
919
+ type: 'string',
920
+ description: '按 loginId/关键词搜索、借用并登录 TDBank 账号',
921
+ },
922
+ tabId: {
923
+ type: 'number',
924
+ description: 'CDP 控制浏览器中用于读取 TDBank Cookie 的标签页 ID',
925
+ },
926
+ copyCookieDomains: {
927
+ type: 'array',
928
+ items: { type: 'string' },
929
+ description: '从控制浏览器复制到新实例的 Cookie 域名列表。保留 HttpOnly/Secure/SameSite/过期时间等属性;非内置域需先授权。可与 TDBank 登录组合,复制先执行、TDBank SSO 后执行。',
930
+ },
931
+ },
932
+ required: ['name'],
933
+ },
934
+ },
935
+ {
936
+ name: 'browser_close',
937
+ description: '关闭一个托管浏览器;临时 Profile 自动删除,持久化 Profile 可显式删除。',
938
+ inputSchema: {
939
+ type: 'object',
940
+ properties: {
941
+ browser: {
942
+ type: 'string',
943
+ description: 'browser_launch 返回的 browserId 或实例名称',
944
+ },
945
+ deleteProfile: {
946
+ type: 'boolean',
947
+ description: '是否强制删除持久化 Profile,默认 false',
948
+ default: false,
949
+ },
950
+ },
951
+ required: ['browser'],
952
+ },
953
+ },
954
+ {
955
+ name: 'browser_tab_open',
956
+ description: '打开或按 URL 规则复用标签页。默认新建并等待加载完成;reuse=prefer 可优先复用,reuse=require 可限制为只复用。响应的 opened/reused 可用于判断流程结束时是否应关闭该 Tab。',
957
+ inputSchema: {
958
+ type: 'object',
959
+ properties: {
960
+ url: {
961
+ type: 'string',
962
+ description: '要打开的页面 URL',
963
+ },
964
+ active: {
965
+ type: 'boolean',
966
+ description: '是否激活(前台显示)新标签页。默认 false(后台打开)',
967
+ default: false,
968
+ },
969
+ closeExisting: {
970
+ type: 'boolean',
971
+ description: '打开前是否先关闭 Agent 受控的已有标签页。默认 false',
972
+ default: false,
973
+ },
974
+ group: {
975
+ type: 'string',
976
+ description: '把新标签页加入指定标题的标签页分组(同窗口同名分组存在则加入,否则新建)。仅扩展模式支持,CDP 模式忽略',
977
+ },
978
+ reuse: {
979
+ type: 'string',
980
+ enum: ['never', 'prefer', 'require'],
981
+ description: 'never 始终新建;prefer 匹配到则复用、否则新建;require 只允许复用。默认 never',
982
+ default: 'never',
983
+ },
984
+ match: {
985
+ type: 'object',
986
+ description: '复用标签页时的 URL 匹配规则',
987
+ properties: {
988
+ urlPattern: {
989
+ type: 'string',
990
+ description: '支持 * 的完整 URL glob;省略时精确匹配 url',
991
+ },
992
+ ignoreSearch: {
993
+ type: 'boolean',
994
+ description: '匹配时是否忽略 URL query,默认 false',
995
+ },
996
+ ignoreHash: {
997
+ type: 'boolean',
998
+ description: '匹配时是否忽略 URL hash,默认 false',
999
+ },
1000
+ onMultiple: {
1001
+ type: 'string',
1002
+ enum: ['error', 'first'],
1003
+ description: '匹配多个 Tab 时默认报错;first 显式取第一个',
1004
+ },
1005
+ },
1006
+ },
1007
+ restoreFocusOnClose: {
1008
+ type: 'boolean',
1009
+ description: '新建 active Tab 关闭后是否恢复原活动标签页,默认 false',
1010
+ default: false,
1011
+ },
1012
+ emulation: {
1013
+ type: 'object',
1014
+ description: '开 tab 即进入移动端模拟态(视口/DPR/UA/触摸),等价于随后立即调用 browser_set_device_emulation,但在导航前生效,页面首屏就是移动态、无需 reload。字段规则与 browser_set_device_emulation 一致(无 enabled,开 tab 即启用)。',
1015
+ properties: {
1016
+ preset: {
1017
+ type: 'string',
1018
+ enum: ['iphone-15-pro', 'iphone-se', 'pixel-8', 'ipad-mini', 'galaxy-s23'],
1019
+ description: '内置设备预设,一键提供 viewport/DPR/UA。可与下面字段组合,显式字段覆盖预设值',
1020
+ },
1021
+ width: { type: 'number', description: '视口宽度(覆盖 preset)' },
1022
+ height: { type: 'number', description: '视口高度(覆盖 preset)' },
1023
+ deviceScaleFactor: { type: 'number', description: 'devicePixelRatio(覆盖 preset)' },
1024
+ mobile: {
1025
+ type: 'boolean',
1026
+ description: '是否启用移动渲染模式。默认从 preset 推断,无 preset 默认 true',
1027
+ },
1028
+ userAgent: {
1029
+ type: 'string',
1030
+ description: '自定义 User-Agent(覆盖 preset)。无 preset 时必填',
1031
+ },
1032
+ orientation: {
1033
+ type: 'string',
1034
+ enum: ['portrait', 'landscape'],
1035
+ description: '横竖屏,landscape 会对调宽高。默认 portrait',
1036
+ },
1037
+ touch: {
1038
+ type: 'boolean',
1039
+ description: '是否启用触摸事件并把鼠标事件转成 touch。默认 mobile 为真时启用',
1040
+ },
1041
+ },
1042
+ },
1043
+ },
1044
+ required: ['url'],
1045
+ },
1046
+ },
1047
+ {
1048
+ name: 'browser_tab_close',
1049
+ description: '关闭指定的浏览器标签页。',
1050
+ inputSchema: {
1051
+ type: 'object',
1052
+ properties: {
1053
+ tabId: {
1054
+ type: 'number',
1055
+ description: '要关闭的标签页 ID(从 tab_open 或 tab_list 获取)',
1056
+ },
1057
+ },
1058
+ required: ['tabId'],
1059
+ },
1060
+ },
1061
+ {
1062
+ name: 'browser_tab_list',
1063
+ description: '列出当前浏览器窗口中所有标签页的信息,包括 ID、URL、标题和是否活动。',
1064
+ inputSchema: {
1065
+ type: 'object',
1066
+ properties: {},
1067
+ },
1068
+ },
1069
+ {
1070
+ name: 'browser_page_click',
1071
+ description: '点击页面元素,默认内置观察:同一次调用内等待相关 DOM、XHR/Fetch/MTop 请求或导航稳定,返回 { action, observation };传 observe:false 仅执行裸点击。定位方式三选一:selector(CSS 或 @eN snapshot ref)、text(按可见文本匹配 accessibility name)、point(视口坐标)。selector/@ref 默认走 el.click();clickType:"cdp" 时改用真实 mouse/touch 事件。text/point 始终使用真实输入事件。',
1072
+ inputSchema: {
1073
+ type: 'object',
1074
+ properties: {
1075
+ tabId: {
1076
+ type: 'number',
1077
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1078
+ },
1079
+ selector: {
1080
+ type: 'string',
1081
+ description: '原生 CSS 选择器(如 "[aria-label=\'提交\']"、"#login-btn")或 snapshot ref(如 "@e3",必须带 "@" 前缀)。注意字段名是 "selector" 而非 "ref"。不支持 :has-text/:visible 等 Playwright 扩展语法。与 text/point 互斥。',
1082
+ },
1083
+ text: {
1084
+ type: 'string',
1085
+ description: '按可见文本(accessibility name)定位。例如 "退款明细"。匹配到多个时会报错并列出候选——可加 role 或 exact:true 收敛,或用 page_snapshot 拿 @ref。与 selector/point 互斥。',
1086
+ },
1087
+ point: {
1088
+ type: 'object',
1089
+ description: '按视口坐标点击(CSS 像素,左上角为原点)。selector 和 text 都不适用时使用。与 selector/text 互斥。',
1090
+ properties: {
1091
+ x: { type: 'number', description: '横坐标(CSS 像素)' },
1092
+ y: { type: 'number', description: '纵坐标(CSS 像素)' },
1093
+ },
1094
+ required: ['x', 'y'],
1095
+ },
1096
+ role: {
1097
+ type: 'string',
1098
+ description: '配合 text 使用,按 AX role 过滤消歧(如 "button"、"link"、"heading"、"StaticText")',
1099
+ },
1100
+ exact: {
1101
+ type: 'boolean',
1102
+ description: '配合 text 使用,默认 true 精确匹配;false 时按子串匹配',
1103
+ default: true,
1104
+ },
1105
+ clickType: {
1106
+ type: 'string',
1107
+ enum: ['js', 'cdp'],
1108
+ description: '点击方式。默认 js:对 selector/@ref 调用 el.click();cdp:派发真实 mouse/touch 事件。text/point 始终用 cdp',
1109
+ default: 'js',
1110
+ },
1111
+ observe: OBSERVE_INPUT_SCHEMA,
1112
+ },
1113
+ required: ['tabId'],
1114
+ },
1115
+ },
1116
+ {
1117
+ name: 'browser_page_type',
1118
+ description: '向输入框填写文本,默认内置观察(校验提示等异步变化),返回 { action, observation };传 observe:false 仅执行裸输入。兼容 React 等框架的受控组件,会自动触发 input 和 change 事件。selector 支持原生 CSS 或 "@eN" snapshot ref。',
1119
+ inputSchema: {
1120
+ type: 'object',
1121
+ properties: {
1122
+ tabId: {
1123
+ type: 'number',
1124
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1125
+ },
1126
+ selector: {
1127
+ type: 'string',
1128
+ description: '原生 CSS 选择器或 snapshot ref(如 "@e6",必须带 "@" 前缀)。字段名是 "selector"。不支持 :has-text/:visible 等 Playwright 扩展。',
1129
+ },
1130
+ text: {
1131
+ type: 'string',
1132
+ description: '要输入的文本内容',
1133
+ },
1134
+ clearFirst: {
1135
+ type: 'boolean',
1136
+ description: '是否先清空输入框原有内容。默认 true',
1137
+ default: true,
1138
+ },
1139
+ observe: OBSERVE_INPUT_SCHEMA,
1140
+ },
1141
+ required: ['tabId', 'selector', 'text'],
1142
+ },
1143
+ },
1144
+ {
1145
+ name: 'browser_page_scroll',
1146
+ description: '滚动页面,默认内置观察(懒加载内容等变化),返回 { action, observation };传 observe:false 仅执行裸滚动。支持 up、down、top、bottom 四个方向。',
1147
+ inputSchema: {
1148
+ type: 'object',
1149
+ properties: {
1150
+ tabId: {
1151
+ type: 'number',
1152
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1153
+ },
1154
+ direction: {
1155
+ type: 'string',
1156
+ enum: ['up', 'down', 'top', 'bottom'],
1157
+ description: '滚动方向',
1158
+ },
1159
+ distance: {
1160
+ type: 'number',
1161
+ description: '滚动距离(像素),仅 up/down 方向有效。默认 600',
1162
+ default: 600,
1163
+ },
1164
+ observe: OBSERVE_INPUT_SCHEMA,
1165
+ },
1166
+ required: ['tabId', 'direction'],
1167
+ },
1168
+ },
1169
+ {
1170
+ name: 'browser_page_hover',
1171
+ description: '将鼠标悬停在元素上,默认内置观察(下拉菜单、提示等变化),返回 { action, observation };传 observe:false 仅执行裸悬停。使用 CDP Input.dispatchMouseEvent 发送真实鼠标移动事件,selector 支持原生 CSS 或 "@eN" snapshot ref。',
1172
+ inputSchema: {
1173
+ type: 'object',
1174
+ properties: {
1175
+ tabId: {
1176
+ type: 'number',
1177
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1178
+ },
1179
+ selector: {
1180
+ type: 'string',
1181
+ description: '原生 CSS 选择器或 snapshot ref(如 "@e1",必须带 "@" 前缀)。字段名是 "selector"。不支持 :has-text/:visible 等 Playwright 扩展。',
1182
+ },
1183
+ observe: OBSERVE_INPUT_SCHEMA,
1184
+ },
1185
+ required: ['tabId', 'selector'],
1186
+ },
1187
+ },
1188
+ {
1189
+ name: 'browser_page_eval',
1190
+ description: '在页面上下文中执行任意 JavaScript 表达式并返回结果。适用于:获取页面数据、操作 DOM、调用页面 API 等。',
1191
+ inputSchema: {
1192
+ type: 'object',
1193
+ properties: {
1194
+ tabId: {
1195
+ type: 'number',
1196
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1197
+ },
1198
+ expression: {
1199
+ type: 'string',
1200
+ description: '要执行的 JavaScript 表达式',
1201
+ },
1202
+ frameId: {
1203
+ type: 'string',
1204
+ description: '目标 frame 的 CDP frameId。不传则在主 frame 中执行。可通过 browser_page_frames 获取',
1205
+ },
1206
+ awaitPromise: {
1207
+ type: 'boolean',
1208
+ description: '是否等待表达式返回的 Promise,默认 true',
1209
+ default: true,
1210
+ },
1211
+ },
1212
+ required: ['tabId', 'expression'],
1213
+ },
1214
+ },
1215
+ {
1216
+ name: 'browser_page_snapshot',
1217
+ description: '获取页面无障碍树,返回角色、名称、值、状态和 @ref。默认 mode="full" 建立完整基线;长列表可用 mode="viewport",表单可用 mode="interactive",已知弹层或区域可用 rootSelector 获取局部子树。页面缺少 AX 控件语义时 interactive 会自动回退到 viewport,并返回 fallbackMode="viewport"。后续交互依赖页面交互工具内置的观察结果,仅在建议回退、导航或 ref 失效时再次调用。',
1218
+ inputSchema: {
1219
+ type: 'object',
1220
+ properties: {
1221
+ tabId: {
1222
+ type: 'number',
1223
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1224
+ },
1225
+ depth: {
1226
+ type: 'number',
1227
+ description: '无障碍树的最大深度。默认 15,增大可获取更深层元素',
1228
+ default: 15,
1229
+ },
1230
+ mode: {
1231
+ type: 'string',
1232
+ enum: ['full', 'interactive', 'viewport'],
1233
+ description: '输出范围:full 为完整树;interactive 优先保留语义控件,无 AX 控件语义时自动回退到 viewport;viewport 只保留当前可视区域。默认 full',
1234
+ },
1235
+ rootSelector: {
1236
+ type: 'string',
1237
+ description: '只获取指定 CSS 元素的 AX 子树,可与 mode 组合',
1238
+ },
1239
+ maxNodes: {
1240
+ type: 'number',
1241
+ description: '最多输出节点数;超出时保留首尾。0 表示不限制',
1242
+ },
1243
+ maxChars: {
1244
+ type: 'number',
1245
+ description: '最多输出字符数,默认 60000;超出时保留首尾。0 表示不限制',
1246
+ },
1247
+ },
1248
+ required: ['tabId'],
1249
+ },
1250
+ },
1251
+ {
1252
+ name: 'browser_page_press',
1253
+ description: '在页面中按下键盘按键,默认内置观察(提交、关闭弹窗等结果),返回 { action, observation };传 observe:false 仅执行裸按键。支持单个按键和组合键。',
1254
+ inputSchema: {
1255
+ type: 'object',
1256
+ properties: {
1257
+ tabId: {
1258
+ type: 'number',
1259
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1260
+ },
1261
+ key: {
1262
+ type: 'string',
1263
+ description: '按键名称。支持:Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, Space',
1264
+ },
1265
+ modifiers: {
1266
+ type: 'array',
1267
+ items: { type: 'string' },
1268
+ description: '修饰键数组,如 ["Alt", "Shift", "Control", "Meta"]',
1269
+ },
1270
+ observe: OBSERVE_INPUT_SCHEMA,
1271
+ },
1272
+ required: ['tabId', 'key'],
1273
+ },
1274
+ },
1275
+ {
1276
+ name: 'browser_page_wait',
1277
+ description: '独立等待接口:仅用于没有前置动作时等待指定时间、已知 selector 或页面 condition。三种模式必须且只能提供一个;点击、输入后的异步稳定判断由页面交互工具内置的观察完成。',
1278
+ inputSchema: {
1279
+ type: 'object',
1280
+ properties: {
1281
+ tabId: {
1282
+ type: 'number',
1283
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1284
+ },
1285
+ time: {
1286
+ type: 'number',
1287
+ description: '固定等待毫秒数,与 selector/condition 三选一',
1288
+ },
1289
+ selector: {
1290
+ type: 'string',
1291
+ description: 'CSS 选择器,等待该元素出现在 DOM 中,与 time/condition 三选一',
1292
+ },
1293
+ condition: {
1294
+ type: 'string',
1295
+ description: '在页面主上下文轮询的 JavaScript 表达式,返回 truthy 时结束,支持 Promise;与 time/selector 三选一',
1296
+ },
1297
+ timeout: {
1298
+ type: 'number',
1299
+ description: '等待超时时间(毫秒),默认 10000ms',
1300
+ default: 10000,
1301
+ },
1302
+ interval: {
1303
+ type: 'number',
1304
+ description: 'selector/condition 的轮询间隔(毫秒),默认 200ms',
1305
+ default: 200,
1306
+ },
1307
+ },
1308
+ required: ['tabId'],
1309
+ },
1310
+ },
1311
+ {
1312
+ name: 'browser_page_navigate',
1313
+ description: '在指定标签页中导航到新的 URL;或设置 refresh:true 直接重新加载当前页面(无需 url)。返回导航结果,可用于验证页面是否成功加载。',
1314
+ inputSchema: {
1315
+ type: 'object',
1316
+ properties: {
1317
+ tabId: {
1318
+ type: 'number',
1319
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1320
+ },
1321
+ url: {
1322
+ type: 'string',
1323
+ description: '要导航到的 URL。refresh 为 true 时可省略(此时刷新当前页面)',
1324
+ },
1325
+ refresh: {
1326
+ type: 'boolean',
1327
+ description: '为 true 时直接重新加载当前页面(忽略 url),用于刷新页面。默认 false',
1328
+ default: false,
1329
+ },
1330
+ },
1331
+ required: ['tabId'],
1332
+ },
1333
+ },
1334
+ {
1335
+ name: 'browser_page_upload',
1336
+ description: '向 <input type="file"> 上传单个或多个文件(通过 CDP DOM.setFileInputFiles),默认内置观察(上传状态变化),返回 { action, observation };传 observe:false 仅执行裸上传。selector 支持原生 CSS 或 "@eN" snapshot ref。',
1337
+ inputSchema: {
1338
+ type: 'object',
1339
+ properties: {
1340
+ tabId: {
1341
+ type: 'number',
1342
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1343
+ },
1344
+ selector: {
1345
+ type: 'string',
1346
+ description: '原生 CSS 选择器或 snapshot ref(如 "@e5",必须带 "@" 前缀),用于定位 <input type="file"> 元素。字段名是 "selector"。',
1347
+ },
1348
+ filePaths: {
1349
+ type: 'array',
1350
+ items: { type: 'string' },
1351
+ description: '要上传的本地文件路径数组,如 ["/Users/me/photo.jpg"]',
1352
+ },
1353
+ observe: OBSERVE_INPUT_SCHEMA,
1354
+ },
1355
+ required: ['tabId', 'selector', 'filePaths'],
1356
+ },
1357
+ },
1358
+ {
1359
+ name: 'browser_page_frames',
1360
+ description: '列出当前页面中所有 frame(主 frame + iframe)。返回每个 frame 的 frameId、URL、名称等信息。适用于:了解页面是否包含 iframe、获取 iframe 的 frameId 以便在 browser_page_eval 中指定执行上下文。',
1361
+ inputSchema: {
1362
+ type: 'object',
1363
+ properties: {
1364
+ tabId: {
1365
+ type: 'number',
1366
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1367
+ },
1368
+ },
1369
+ required: ['tabId'],
1370
+ },
1371
+ },
1372
+ {
1373
+ name: 'browser_set_device_emulation',
1374
+ description: '把目标标签页切换为移动端模拟态:视口尺寸 / DPR / UA / 触摸事件全部按指定设备生效,等价于 Chrome DevTools 设备工具栏,作用域仅限该 tab,会话结束自动还原。适用于:调试 H5 移动页面、让 page_click 在 swiper/fastclick 等只听 touchstart 的页面生效、让服务端按移动 UA 返回数据。启用后默认 reload 一次让页面以移动模式重新初始化。enabled=false 清除模拟。',
1375
+ inputSchema: {
1376
+ type: 'object',
1377
+ properties: {
1378
+ enabled: {
1379
+ type: 'boolean',
1380
+ description: '必填。true=启用模拟(按 preset/参数生效),false=清除模拟回到桌面态',
1381
+ },
1382
+ preset: {
1383
+ type: 'string',
1384
+ enum: ['iphone-15-pro', 'iphone-se', 'pixel-8', 'ipad-mini', 'galaxy-s23'],
1385
+ description: '内置设备预设,一键提供 viewport/DPR/UA。可与下面的字段组合,显式字段会覆盖预设值',
1386
+ },
1387
+ width: { type: 'number', description: '视口宽度(覆盖 preset)' },
1388
+ height: { type: 'number', description: '视口高度(覆盖 preset)' },
1389
+ deviceScaleFactor: { type: 'number', description: 'devicePixelRatio(覆盖 preset)' },
1390
+ mobile: {
1391
+ type: 'boolean',
1392
+ description: '是否启用移动渲染模式(影响 hover/pointer media query、viewport meta 行为)。默认从 preset 推断,无 preset 默认 true',
1393
+ },
1394
+ userAgent: { type: 'string', description: '自定义 User-Agent(覆盖 preset)' },
1395
+ orientation: {
1396
+ type: 'string',
1397
+ enum: ['portrait', 'landscape'],
1398
+ description: '横竖屏,landscape 会对调宽高。默认 portrait',
1399
+ },
1400
+ touch: {
1401
+ type: 'boolean',
1402
+ description: '是否启用触摸事件并把鼠标事件转成 touch。默认 mobile 为真时启用',
1403
+ },
1404
+ reload: {
1405
+ type: 'boolean',
1406
+ description: '启用模拟后是否重载页面(让 JS 读到新的 navigator.userAgent / window.innerWidth)。默认 true',
1407
+ },
1408
+ tabId: {
1409
+ type: 'number',
1410
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1411
+ },
1412
+ },
1413
+ required: ['tabId', 'enabled'],
1414
+ },
1415
+ },
1416
+ {
1417
+ name: 'browser_set_timezone',
1418
+ description: '覆盖目标标签页的时区,作用域仅限该 tab,立即影响 Date / Intl 等页面时间 API。timezone 使用 IANA 时区标识(如 Asia/Shanghai、America/New_York);传空字符串恢复浏览器默认时区。',
1419
+ inputSchema: {
1420
+ type: 'object',
1421
+ properties: {
1422
+ tabId: {
1423
+ type: 'number',
1424
+ description: '目标标签页 ID(从 browser_tab_open 或 browser_tab_list 获取)',
1425
+ },
1426
+ timezone: {
1427
+ type: 'string',
1428
+ description: 'IANA 时区标识;空字符串表示清除覆盖',
1429
+ },
1430
+ },
1431
+ required: ['tabId', 'timezone'],
1432
+ },
1433
+ },
1434
+ {
1435
+ name: 'browser_run_actions',
1436
+ description: '批量顺序执行多个无需中间判断的原子操作,适合 hover→click 等必须紧密连续的序列。步骤内的页面动作不带内置观察;遇到第一个失败步骤立即停止,且不能嵌套 run_actions。若下一步需要根据页面变化决定,优先逐步使用带观察的页面交互工具;不要用本工具固化「点击→等待→截图」循环。',
1437
+ inputSchema: {
1438
+ type: 'object',
1439
+ properties: {
1440
+ actions: {
1441
+ type: 'array',
1442
+ description: '要依次执行的 action 列表',
1443
+ items: {
1444
+ type: 'object',
1445
+ properties: {
1446
+ action: {
1447
+ type: 'string',
1448
+ description: 'action 名称(不带 browser_ 前缀),如 page_hover、page_click;仅组合无需中间判断的步骤',
1449
+ },
1450
+ data: {
1451
+ type: 'object',
1452
+ description: '该 action 的参数(与单独调用时相同)',
1453
+ },
1454
+ },
1455
+ required: ['action'],
1456
+ },
1457
+ },
1458
+ tabId: {
1459
+ type: 'number',
1460
+ description: '所有步骤共享的目标标签页 ID。步骤自身指定的 tabId 优先级更高',
1461
+ },
1462
+ },
1463
+ required: ['tabId', 'actions'],
1464
+ },
1465
+ },
1466
+ {
1467
+ name: 'browser_list',
1468
+ description: '列出所有浏览器。返回普通浏览器和托管浏览器的 browserId、名称、类型、连接状态及当前默认目标;托管浏览器还包含运行模式、持久化设置和登录账号。只有 status=connected 的浏览器可用于后续操作。',
1469
+ inputSchema: {
1470
+ type: 'object',
1471
+ properties: {},
1472
+ },
1473
+ },
1474
+ {
1475
+ name: 'browser_select_browser',
1476
+ description: '选择后续所有浏览器操作的默认目标浏览器(有多个浏览器连接时)。传入 browser_list 返回且 status=connected 的 browserId 或名称即可锁定;传空字符串则清除锁定,回到自动模式(操作最近聚焦的浏览器)。',
1477
+ inputSchema: {
1478
+ type: 'object',
1479
+ properties: {
1480
+ browser: {
1481
+ type: 'string',
1482
+ description: '目标浏览器的 browserId / 名称;空字符串表示清除锁定、恢复自动选择',
1483
+ },
1484
+ },
1485
+ required: ['browser'],
1486
+ },
1487
+ },
1488
+ ];
1489
+ // 处理工具列表请求
1490
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
1491
+ return { tools };
1492
+ });
1493
+ // 处理工具调用请求
1494
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1495
+ const { name, arguments: args } = request.params;
1496
+ try {
1497
+ let result;
1498
+ switch (name) {
1499
+ case 'browser_get_requests': {
1500
+ const { count = 10, filter, since, includeBody = true, includeHeaders = false, maxBodySize = 50000, fields, keysOnly = false, source, } = args;
1501
+ result = await client.getRequests({
1502
+ count,
1503
+ filter,
1504
+ since,
1505
+ includeBody,
1506
+ includeHeaders,
1507
+ maxBodySize,
1508
+ fields,
1509
+ keysOnly,
1510
+ source,
1511
+ });
1512
+ break;
1513
+ }
1514
+ case 'browser_get_events': {
1515
+ const { source = 'all', event_type, filter, since, limit = 20 } = args;
1516
+ result = await client.getEvents({ source, event_type, filter, since, limit });
1517
+ break;
1518
+ }
1519
+ case 'browser_get_logs': {
1520
+ const { level = 'all', since, limit = 100, filter } = args;
1521
+ result = await client.getLogs({ level, since, limit, filter });
1522
+ break;
1523
+ }
1524
+ case 'browser_set_mock': {
1525
+ const { payloadFile } = args;
1526
+ let { apiName, mockData, fields, enabled, duration, conditions } = args;
1527
+ enabled = enabled ?? true;
1528
+ // 如果提供了 payloadFile,从文件读取 mock 数据
1529
+ if (payloadFile) {
1530
+ if (!existsSync(payloadFile)) {
1531
+ throw new Error(`Payload file not found: ${payloadFile}`);
1532
+ }
1533
+ const fileContent = readFileSync(payloadFile, 'utf-8');
1534
+ const fileData = JSON.parse(fileContent);
1535
+ // 文件内容可以包含 apiName、mockData、fields、enabled、duration、conditions 等参数
1536
+ if (fileData.apiName)
1537
+ apiName = fileData.apiName;
1538
+ if (fileData.mockData !== undefined)
1539
+ mockData = fileData.mockData;
1540
+ if (fileData.fields !== undefined)
1541
+ fields = fileData.fields;
1542
+ if (fileData.enabled !== undefined)
1543
+ enabled = fileData.enabled;
1544
+ if (fileData.duration !== undefined)
1545
+ duration = fileData.duration;
1546
+ if (fileData.conditions !== undefined)
1547
+ conditions = fileData.conditions;
1548
+ }
1549
+ result = await client.setMock({
1550
+ apiName,
1551
+ mockData,
1552
+ fields,
1553
+ enabled,
1554
+ duration,
1555
+ conditions,
1556
+ });
1557
+ break;
1558
+ }
1559
+ case 'browser_get_mocks': {
1560
+ const { apiName } = args;
1561
+ result = await client.getMocks({ apiName });
1562
+ break;
1563
+ }
1564
+ case 'browser_get_api_schema': {
1565
+ const { api, version = '1.0', fields } = args;
1566
+ result = await client.getApiSchema({ api, version, fields });
1567
+ break;
1568
+ }
1569
+ case 'browser_send_request': {
1570
+ result = await client.sendRequest(args);
1571
+ break;
1572
+ }
1573
+ case 'browser_send_mtop_request': {
1574
+ const { api, data, version, method, appKey, env, timeout } = args;
1575
+ result = await client.sendMtopRequest({
1576
+ api,
1577
+ data,
1578
+ version,
1579
+ method,
1580
+ appKey,
1581
+ env,
1582
+ timeout,
1583
+ });
1584
+ break;
1585
+ }
1586
+ case 'browser_set_rule': {
1587
+ result = await client.setRule(args);
1588
+ break;
1589
+ }
1590
+ case 'browser_get_rules': {
1591
+ result = await client.getRules();
1592
+ break;
1593
+ }
1594
+ case 'browser_page_screenshot': {
1595
+ const { format = 'png', quality = 80, selector, tabId } = args;
1596
+ result = await client.pageScreenshot({ format, quality, selector, tabId });
1597
+ // 截图工具直接返回 image 类型的 content,符合 MCP 规范
1598
+ return {
1599
+ content: [
1600
+ {
1601
+ type: 'image',
1602
+ data: result.data,
1603
+ mimeType: result.meta.format === 'jpeg' ? 'image/jpeg' : 'image/png',
1604
+ },
1605
+ {
1606
+ type: 'text',
1607
+ text: `Screenshot captured: ${result.meta.width}x${result.meta.height} ${result.meta.format.toUpperCase()} (${Math.round(result.meta.size / 1024)}KB)\nPage: ${result.meta.title}\nURL: ${result.meta.url}`,
1608
+ },
1609
+ ],
1610
+ };
1611
+ }
1612
+ case 'browser_page_selected_element': {
1613
+ const { includeOuterHTML = true, includeScreenshot = false, tabId } = args;
1614
+ result = await client.pageSelectedElement({
1615
+ includeOuterHTML,
1616
+ includeScreenshot,
1617
+ tabId,
1618
+ });
1619
+ if (includeScreenshot && result?.screenshot?.data) {
1620
+ return {
1621
+ content: [
1622
+ {
1623
+ type: 'image',
1624
+ data: result.screenshot.data,
1625
+ mimeType: result.screenshot.meta?.format === 'jpeg' ? 'image/jpeg' : 'image/png',
1626
+ },
1627
+ {
1628
+ type: 'text',
1629
+ text: `Selected element screenshot captured: ${result.screenshot.meta?.width}x${result.screenshot.meta?.height} ${String(result.screenshot.meta?.format || '').toUpperCase()}\nElement: ${result.tagName || 'unknown'}\nSelector: ${result.selector || 'N/A'}`,
1630
+ },
1631
+ ],
1632
+ };
1633
+ }
1634
+ break;
1635
+ }
1636
+ case 'get_user_info': {
1637
+ const { detail = false } = args;
1638
+ result = await client.getUserInfo({ detail });
1639
+ break;
1640
+ }
1641
+ case 'tdbank_account': {
1642
+ result = await client.tdbankAccount(args);
1643
+ break;
1644
+ }
1645
+ case 'browser_launch': {
1646
+ result = await client.browserLaunch(args);
1647
+ break;
1648
+ }
1649
+ case 'browser_close': {
1650
+ result = await client.browserClose(args);
1651
+ break;
1652
+ }
1653
+ case 'browser_tab_open': {
1654
+ const { url, active = false, closeExisting = false, reuse = 'never', match, restoreFocusOnClose = false, emulation, group, } = args;
1655
+ result = await client.tabOpen({
1656
+ url,
1657
+ active,
1658
+ closeExisting,
1659
+ reuse,
1660
+ match,
1661
+ restoreFocusOnClose,
1662
+ emulation,
1663
+ group,
1664
+ });
1665
+ break;
1666
+ }
1667
+ case 'browser_tab_close': {
1668
+ const { tabId } = args;
1669
+ result = await client.tabClose({ tabId });
1670
+ break;
1671
+ }
1672
+ case 'browser_tab_list': {
1673
+ result = await client.tabList();
1674
+ break;
1675
+ }
1676
+ case 'browser_page_click': {
1677
+ const { tabId, selector, text, point, role, exact, clickType, observe } = args;
1678
+ result = await client.pageClick({
1679
+ tabId,
1680
+ selector,
1681
+ text,
1682
+ point,
1683
+ role,
1684
+ exact,
1685
+ clickType,
1686
+ observe,
1687
+ });
1688
+ break;
1689
+ }
1690
+ case 'browser_page_type': {
1691
+ const { tabId, selector, text, clearFirst = true, observe } = args;
1692
+ result = await client.pageType({ tabId, selector, text, clearFirst, observe });
1693
+ break;
1694
+ }
1695
+ case 'browser_page_scroll': {
1696
+ const { tabId, direction, distance = 600, observe } = args;
1697
+ result = await client.pageScroll({ tabId, direction, distance, observe });
1698
+ break;
1699
+ }
1700
+ case 'browser_page_hover': {
1701
+ const { tabId, selector, observe } = args;
1702
+ result = await client.pageHover({ tabId, selector, observe });
1703
+ break;
1704
+ }
1705
+ case 'browser_page_eval': {
1706
+ const { tabId, expression, frameId, awaitPromise = true } = args;
1707
+ result = await client.pageEval({ tabId, expression, frameId, awaitPromise });
1708
+ break;
1709
+ }
1710
+ case 'browser_page_snapshot': {
1711
+ const { tabId, depth = 15, mode, rootSelector, maxNodes, maxChars } = args;
1712
+ result = await client.pageSnapshot({
1713
+ tabId,
1714
+ depth,
1715
+ mode,
1716
+ rootSelector,
1717
+ maxNodes,
1718
+ maxChars,
1719
+ });
1720
+ break;
1721
+ }
1722
+ case 'browser_page_press': {
1723
+ const { tabId, key, modifiers, observe } = args;
1724
+ result = await client.pagePress({ tabId, key, modifiers, observe });
1725
+ break;
1726
+ }
1727
+ case 'browser_page_wait': {
1728
+ const { tabId, time, selector, condition, timeout = 10000, interval = 200 } = args;
1729
+ result = await client.pageWait({
1730
+ tabId,
1731
+ time,
1732
+ selector,
1733
+ condition,
1734
+ timeout,
1735
+ interval,
1736
+ });
1737
+ break;
1738
+ }
1739
+ case 'browser_page_navigate': {
1740
+ const { tabId, url, refresh } = args;
1741
+ result = await client.pageNavigate({ tabId, url, refresh });
1742
+ break;
1743
+ }
1744
+ case 'browser_page_upload': {
1745
+ const { tabId, selector, filePaths, observe } = args;
1746
+ result = await client.pageUpload({ tabId, selector, filePaths, observe });
1747
+ break;
1748
+ }
1749
+ case 'browser_page_frames': {
1750
+ const { tabId } = args;
1751
+ result = await client.pageFrames({ tabId });
1752
+ break;
1753
+ }
1754
+ case 'browser_set_device_emulation': {
1755
+ result = await client.setDeviceEmulation(args);
1756
+ break;
1757
+ }
1758
+ case 'browser_set_timezone': {
1759
+ result = await client.setTimezone(args);
1760
+ break;
1761
+ }
1762
+ case 'browser_run_actions': {
1763
+ const { actions, tabId } = args;
1764
+ result = await client.runActions({ actions, tabId });
1765
+ if (result?.results?.length > 0) {
1766
+ const screenshots = result.results.filter((r) => r.success && r.action === 'page_screenshot' && r.data?.data);
1767
+ if (screenshots.length > 0) {
1768
+ return {
1769
+ content: [
1770
+ ...screenshots.map((r) => ({
1771
+ type: 'image',
1772
+ data: r.data.data,
1773
+ mimeType: r.data.meta?.format === 'jpeg' ? 'image/jpeg' : 'image/png',
1774
+ })),
1775
+ {
1776
+ type: 'text',
1777
+ text: JSON.stringify({
1778
+ ...result,
1779
+ results: result.results.map((r) => r.action === 'page_screenshot' && r.success
1780
+ ? { ...r, data: { meta: r.data?.meta } }
1781
+ : r),
1782
+ }, null, 2),
1783
+ },
1784
+ ],
1785
+ };
1786
+ }
1787
+ }
1788
+ break;
1789
+ }
1790
+ case 'browser_list': {
1791
+ result = await client.browserList();
1792
+ break;
1793
+ }
1794
+ case 'browser_select_browser': {
1795
+ result = await client.selectBrowser(args);
1796
+ break;
1797
+ }
1798
+ default:
1799
+ throw new Error(`Unknown tool: ${name}`);
1800
+ }
1801
+ // 使用智能格式化返回结果
1802
+ return {
1803
+ content: formatResponseWithSizeCheck(result, name, args),
1804
+ };
1805
+ }
1806
+ catch (error) {
1807
+ log(`Error executing tool ${name}:`, error);
1808
+ return {
1809
+ content: [
1810
+ {
1811
+ type: 'text',
1812
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
1813
+ },
1814
+ ],
1815
+ isError: true,
1816
+ };
1817
+ }
1818
+ });
1819
+ // 使用 stdio 传输层启动服务器
1820
+ const transport = new StdioServerTransport();
1821
+ await server.connect(transport);
1822
+ log('MCP Server started and ready');
1823
+ }
1824
+ // 错误处理
1825
+ process.on('uncaughtException', error => {
1826
+ console.error('[MCP Server] Uncaught exception:', error);
1827
+ process.exit(1);
1828
+ });
1829
+ process.on('unhandledRejection', error => {
1830
+ console.error('[MCP Server] Unhandled rejection:', error);
1831
+ process.exit(1);
1832
+ });
1833
+ // 启动服务器
1834
+ main().catch(error => {
1835
+ console.error('[MCP Server] Failed to start:', error);
1836
+ process.exit(1);
1837
+ });