@mearl/mcp-server 2.2.0 → 2.2.2
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 +2 -2
- package/dist/configure.d.ts +11 -1
- package/dist/configure.js +98 -53
- package/dist/index.js +48 -8
- package/dist/uninstall.d.ts +28 -1
- package/dist/uninstall.js +96 -75
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
- 📴 **browser_release**: 按用户明确要求释放 debugger/CDP 控制和会话级临时效果,并保留浏览器与全部标签页
|
|
13
13
|
- 🚀 **browser_launch / browser_close**: 启动或关闭独立 Profile 的托管 Chrome
|
|
14
14
|
- 🌐 **browser_page_snapshot**: 首次获取页面结构和可交互元素
|
|
15
|
-
- ⚡ **browser_page_click/type/hover/scroll/press/upload**:
|
|
15
|
+
- ⚡ **browser_page_click/type/hover/scroll/press/upload**: 执行页面动作,默认内置观察,返回通知、可交互控件、焦点、导航或直接打开的新标签页
|
|
16
16
|
|
|
17
17
|
## 浏览器交互工作流
|
|
18
18
|
|
|
19
|
-
首次进入页面时调用 `browser_page_snapshot` 建立结构基线。后续点击、输入、悬停、滚动、按键和上传直接调用 `browser_page_click/type/hover/scroll/press/upload`,它们默认内置观察,动作与异步同步在一次调用内完成;同页结果读取 `effects.notifications`、`effects.interactives` 和 `effects.focus`。新增或变化的可交互节点会尽量携带可直接用于后续动作的 `node.ref`,解析不到 ref 时回退使用 `node.selector`。导航结果读取新 URL
|
|
19
|
+
首次进入页面时调用 `browser_page_snapshot` 建立结构基线。后续点击、输入、悬停、滚动、按键和上传直接调用 `browser_page_click/type/hover/scroll/press/upload`,它们默认内置观察,动作与异步同步在一次调用内完成;同页结果读取 `effects.notifications`、`effects.interactives` 和 `effects.focus`。新增或变化的可交互节点会尽量携带可直接用于后续动作的 `node.ref`,解析不到 ref 时回退使用 `node.selector`。导航结果读取新 URL 和就绪状态;动作直接打开新标签页时,`openedTabs` 返回可继续操作的 `tabId`,无需调用 `browser_tab_list`。通常仅在导航后、`fullSnapshotRecommended: true` 或需要视觉判断时重新 snapshot 或截图;滚动后若下一步需要读取新视口内容,可按需获取 viewport 快照。`browser_page_eval` 默认裸执行,仅在表达式会修改页面且需要变化信号时显式传 `observe` 对象。
|
|
20
20
|
|
|
21
21
|
长列表重新感知时优先使用 `browser_page_snapshot(mode: "viewport")`;已知 CSS 区域时使用 `rootSelector`,已有 ref 时使用 `rootRef`,只查找特定文案或角色时使用 `query`;仅需要当前视口内的控件时使用 `mode: "interactive"`,避免整页 AX Tree 占用上下文。视口内缺少 AX 控件语义时,`interactive` 会自动回退到 viewport,并返回 `fallbackMode: "viewport"`。重复文本点击可用 `scope` 限定子树,滚动子节点所在容器可用 `containerPolicy: "nearest"`。
|
|
22
22
|
|
package/dist/configure.d.ts
CHANGED
|
@@ -5,7 +5,17 @@
|
|
|
5
5
|
* 使用方式:
|
|
6
6
|
* mearl-mcp-configure # 交互式配置(会询问是否使用自定义 Extension ID)
|
|
7
7
|
* mearl-mcp-configure --extension-id=YOUR_ID # 直接指定自定义 Extension ID
|
|
8
|
+
* mearl-mcp-configure --yes # 配置所有已检测客户端,不询问
|
|
8
9
|
* mearl-mcp-configure --skip-mcp # 跳过 MCP 配置
|
|
9
10
|
* mearl-mcp-configure --get # 查看当前配置
|
|
10
11
|
*/
|
|
11
|
-
export {
|
|
12
|
+
export interface ConfigureArgs {
|
|
13
|
+
extensionId?: string;
|
|
14
|
+
get: boolean;
|
|
15
|
+
globalInstall: boolean;
|
|
16
|
+
skipMcp: boolean;
|
|
17
|
+
yes: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseConfigureArgs(args?: string[]): ConfigureArgs;
|
|
20
|
+
export declare function shouldUseNpx(globalInstall: boolean, detectNpxMode?: () => boolean): boolean;
|
|
21
|
+
export declare function runConfigure(argv?: string[]): Promise<void>;
|
package/dist/configure.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* 使用方式:
|
|
6
6
|
* mearl-mcp-configure # 交互式配置(会询问是否使用自定义 Extension ID)
|
|
7
7
|
* mearl-mcp-configure --extension-id=YOUR_ID # 直接指定自定义 Extension ID
|
|
8
|
+
* mearl-mcp-configure --yes # 配置所有已检测客户端,不询问
|
|
8
9
|
* mearl-mcp-configure --skip-mcp # 跳过 MCP 配置
|
|
9
10
|
* mearl-mcp-configure --get # 查看当前配置
|
|
10
11
|
*/
|
|
11
12
|
import { installNativeHost, resolveHostDir } from '@mearl/native-host';
|
|
12
13
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
13
|
-
import {
|
|
14
|
+
import path, { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
14
16
|
import { HOST_NAME, DEFAULT_EXTENSION_ID } from './constants.js';
|
|
15
17
|
import prompts from 'prompts';
|
|
16
18
|
import { MCP_CLIENTS, getMCPServerConfig, isNpxMode, isClientInstalled, } from './mcp-clients.js';
|
|
@@ -19,11 +21,12 @@ const log = (...args) => {
|
|
|
19
21
|
console.log('[Configure]', ...args);
|
|
20
22
|
};
|
|
21
23
|
// 解析命令行参数
|
|
22
|
-
function
|
|
23
|
-
const args = process.argv.slice(2);
|
|
24
|
+
export function parseConfigureArgs(args = process.argv.slice(2)) {
|
|
24
25
|
let extensionId;
|
|
25
26
|
let skipMcp = false;
|
|
26
27
|
let get = false;
|
|
28
|
+
let globalInstall = false;
|
|
29
|
+
let yes = false;
|
|
27
30
|
for (const arg of args) {
|
|
28
31
|
if (arg.startsWith('--extension-id=')) {
|
|
29
32
|
extensionId = arg.split('=')[1];
|
|
@@ -37,8 +40,14 @@ function parseArgs() {
|
|
|
37
40
|
else if (arg === '--get') {
|
|
38
41
|
get = true;
|
|
39
42
|
}
|
|
43
|
+
else if (arg === '--global-install') {
|
|
44
|
+
globalInstall = true;
|
|
45
|
+
}
|
|
46
|
+
else if (arg === '--yes' || arg === '-y' || arg === '--non-interactive') {
|
|
47
|
+
yes = true;
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
|
-
return { extensionId, skipMcp, get };
|
|
50
|
+
return { extensionId, skipMcp, get, globalInstall, yes };
|
|
42
51
|
}
|
|
43
52
|
// 替换或追加 TOML 中的 mcp_servers.xxx 节
|
|
44
53
|
function replaceTOMLSection(content, serverName, newSectionLines) {
|
|
@@ -198,10 +207,11 @@ function showCurrentConfig() {
|
|
|
198
207
|
console.log('\n💡 运行 "mearl-mcp-configure" 重新配置\n');
|
|
199
208
|
}
|
|
200
209
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
export function shouldUseNpx(globalInstall, detectNpxMode = isNpxMode) {
|
|
211
|
+
return !globalInstall && detectNpxMode();
|
|
212
|
+
}
|
|
213
|
+
export async function runConfigure(argv = process.argv.slice(2)) {
|
|
214
|
+
const { extensionId, get, globalInstall, skipMcp, yes } = parseConfigureArgs(argv);
|
|
205
215
|
// 如果是查看配置,显示后退出
|
|
206
216
|
if (get) {
|
|
207
217
|
showCurrentConfig();
|
|
@@ -209,72 +219,101 @@ async function main() {
|
|
|
209
219
|
}
|
|
210
220
|
console.log('🔧 Mearl MCP 配置\n');
|
|
211
221
|
// 检测 npx 模式
|
|
212
|
-
const useNpx =
|
|
222
|
+
const useNpx = shouldUseNpx(globalInstall);
|
|
213
223
|
if (useNpx) {
|
|
214
224
|
log('检测到 npx 模式,将生成 npx 配置');
|
|
215
225
|
}
|
|
226
|
+
else if (globalInstall) {
|
|
227
|
+
log('使用全局 mearl-mcp-server 命令生成配置');
|
|
228
|
+
}
|
|
216
229
|
// 配置 Native Messaging Host
|
|
217
230
|
console.log('\n📦 配置 Chrome Native Messaging Host...');
|
|
218
231
|
// Install both host manifests so MCP and Skills integrations can be used at
|
|
219
232
|
// the same time (they share the same Unix socket and handlers).
|
|
220
|
-
await installNativeHost({
|
|
233
|
+
const nativeHostInstalled = await installNativeHost({
|
|
221
234
|
mode: 'both',
|
|
222
235
|
extensionId: extensionId || process.env.MEARL_EXTENSION_ID || DEFAULT_EXTENSION_ID,
|
|
223
236
|
silent: true,
|
|
224
237
|
});
|
|
238
|
+
if (!nativeHostInstalled) {
|
|
239
|
+
throw new Error('Native Messaging Host 配置失败');
|
|
240
|
+
}
|
|
225
241
|
console.log('✅ 配置完成\n');
|
|
226
242
|
// 配置 MCP 客户端
|
|
227
243
|
if (skipMcp) {
|
|
228
244
|
console.log('跳过 MCP 客户端配置\n');
|
|
229
245
|
}
|
|
230
246
|
else {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
});
|
|
237
|
-
if (response.setupMcp) {
|
|
238
|
-
// 列出全部支持的客户端,已检测到安装的默认预选
|
|
239
|
-
const allClients = Object.entries(MCP_CLIENTS).map(([key, client]) => ({
|
|
240
|
-
title: isClientInstalled(key) ? `${client.name} ✓` : client.name,
|
|
247
|
+
// 列出全部支持的客户端,已检测到安装的默认预选
|
|
248
|
+
const allClients = Object.entries(MCP_CLIENTS).map(([key, client]) => {
|
|
249
|
+
const selected = isClientInstalled(key);
|
|
250
|
+
return {
|
|
251
|
+
title: selected ? `${client.name} ✓` : client.name,
|
|
241
252
|
value: key,
|
|
242
|
-
selected
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
253
|
+
selected,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
const detectedClients = allClients.filter(client => client.selected);
|
|
257
|
+
const detectedNames = detectedClients.map(client => client.title);
|
|
258
|
+
let selectedClients;
|
|
259
|
+
if (yes) {
|
|
260
|
+
if (detectedClients.length === 0) {
|
|
261
|
+
throw new Error('未检测到可配置的 MCP 客户端;请交互式运行 mearl-mcp-configure 进行选择');
|
|
250
262
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
263
|
+
selectedClients = detectedClients.map(client => client.value);
|
|
264
|
+
console.log(`\n将配置已检测到的客户端: ${detectedNames.join(', ')}\n`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const response = await prompts({
|
|
268
|
+
type: 'confirm',
|
|
269
|
+
name: 'setupMcp',
|
|
270
|
+
message: '配置 MCP 客户端?',
|
|
271
|
+
initial: true,
|
|
258
272
|
});
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
273
|
+
if (response.setupMcp === undefined) {
|
|
274
|
+
throw new Error('MCP 客户端配置已取消');
|
|
275
|
+
}
|
|
276
|
+
if (response.setupMcp) {
|
|
277
|
+
if (detectedNames.length > 0) {
|
|
278
|
+
console.log(`\n已检测到: ${detectedNames.join(', ')}\n`);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
console.log('\n未检测到已安装的客户端,可手动选择\n');
|
|
282
|
+
}
|
|
283
|
+
const clientResponse = await prompts({
|
|
284
|
+
type: 'multiselect',
|
|
285
|
+
name: 'clients',
|
|
286
|
+
message: '选择要配置的客户端:',
|
|
287
|
+
choices: allClients,
|
|
288
|
+
instructions: false,
|
|
289
|
+
hint: '空格选择,回车确认',
|
|
290
|
+
});
|
|
291
|
+
selectedClients = clientResponse.clients;
|
|
292
|
+
if (!selectedClients || selectedClients.length === 0) {
|
|
293
|
+
throw new Error('未选择要配置的 MCP 客户端');
|
|
269
294
|
}
|
|
270
|
-
console.log('');
|
|
271
295
|
}
|
|
272
296
|
else {
|
|
273
|
-
console.log('
|
|
297
|
+
console.log('跳过 MCP 客户端配置\n');
|
|
274
298
|
}
|
|
275
299
|
}
|
|
276
|
-
|
|
277
|
-
console.log('
|
|
300
|
+
if (selectedClients && selectedClients.length > 0) {
|
|
301
|
+
console.log('');
|
|
302
|
+
let configured = true;
|
|
303
|
+
for (const clientKey of selectedClients) {
|
|
304
|
+
const client = MCP_CLIENTS[clientKey];
|
|
305
|
+
if (updateMCPConfig(client.configPath, client, useNpx)) {
|
|
306
|
+
console.log(`✅ ${client.name}`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
configured = false;
|
|
310
|
+
console.log(`❌ ${client.name} 配置失败`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
if (!configured) {
|
|
315
|
+
throw new Error('部分 MCP 客户端配置失败');
|
|
316
|
+
}
|
|
278
317
|
}
|
|
279
318
|
}
|
|
280
319
|
console.log('\n✅ 全部配置完成!');
|
|
@@ -283,7 +322,13 @@ async function main() {
|
|
|
283
322
|
console.log(' 2. 打开淘宝/飞猪页面');
|
|
284
323
|
console.log(' 3. 在 AI 中测试: "获取最近 5 个请求"\n');
|
|
285
324
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
325
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
326
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
|
|
327
|
+
try {
|
|
328
|
+
await runConfigure();
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
console.error('Configuration failed:', error);
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -363,7 +363,7 @@ async function main() {
|
|
|
363
363
|
capabilities: {
|
|
364
364
|
tools: {},
|
|
365
365
|
},
|
|
366
|
-
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;interactives/focus 节点有 ref 时优先直接用于后续动作,没有 ref 时使用 selector;notifications 中 change=transient 表示已经消失的 Toast、状态或错误提示。为 navigation 时先检查 ready,ready=true 表示文档、相关业务请求和可见骨架状态已经稳定,可在新页面重建 snapshot。其他情况下通常仅当 fullSnapshotRecommended 为 true 时根据 snapshotReasons 回退,并选择最小范围:已有区域 ref 用 rootRef,已有 CSS 根节点用 rootSelector,只需查找特定目标时加 query,长列表用 mode="viewport",只需当前视口内的控件用 mode="interactive";滚动动作的 moved 只证明位置变化,下一步需要读取新视口内容时可按需获取 viewport 快照。视口内缺少 AX 控件语义时 interactive 会在同一次调用中回退到 viewport,并返回 fallbackMode。重复文本点击用 scope 限定区域,ref 指向滚动容器子节点时用 containerPolicy="nearest"。page_eval 默认裸执行且应作为结构化能力无法表达时的补充,仅在表达式修改页面并需要变化信号时显式传 observe 对象。视觉任务再使用 screenshot。不要在每次动作后固定调用 browser_page_wait、snapshot 或 screenshot。',
|
|
366
|
+
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;interactives/focus 节点有 ref 时优先直接用于后续动作,没有 ref 时使用 selector;notifications 中 change=transient 表示已经消失的 Toast、状态或错误提示。为 navigation 时先检查 ready,ready=true 表示文档、相关业务请求和可见骨架状态已经稳定,可在新页面重建 snapshot。observation.openedTabs 表示动作直接打开的关联标签页,可显式使用返回的 tabId 继续操作,无需调用 browser_tab_list。其他情况下通常仅当 fullSnapshotRecommended 为 true 时根据 snapshotReasons 回退,并选择最小范围:已有区域 ref 用 rootRef,已有 CSS 根节点用 rootSelector,只需查找特定目标时加 query,长列表用 mode="viewport",只需当前视口内的控件用 mode="interactive";滚动动作的 moved 只证明位置变化,下一步需要读取新视口内容时可按需获取 viewport 快照。视口内缺少 AX 控件语义时 interactive 会在同一次调用中回退到 viewport,并返回 fallbackMode。重复文本点击用 scope 限定区域,ref 指向滚动容器子节点时用 containerPolicy="nearest"。page_eval 默认裸执行且应作为结构化能力无法表达时的补充,仅在表达式修改页面并需要变化信号时显式传 observe 对象。视觉任务再使用 screenshot。不要在每次动作后固定调用 browser_page_wait、snapshot 或 screenshot。',
|
|
367
367
|
});
|
|
368
368
|
// 页面交互工具共享的 observe 参数 schema:默认开启内置观察,传 false 关闭
|
|
369
369
|
const OBSERVE_INPUT_SCHEMA = {
|
|
@@ -609,7 +609,7 @@ async function main() {
|
|
|
609
609
|
},
|
|
610
610
|
{
|
|
611
611
|
name: 'browser_send_request',
|
|
612
|
-
description: '通过浏览器发起 HTTP 代理请求,自动携带目标域名的 Cookie(使用当前浏览器登录态)。支持 GET/POST/PUT/DELETE/PATCH
|
|
612
|
+
description: '通过浏览器发起 HTTP 代理请求,自动携带目标域名的 Cookie(使用当前浏览器登录态)。支持 GET/POST/PUT/DELETE/PATCH、multipart 文本字段和本地文件上传,适用于需要用户登录态的接口调试、数据爬取、接口测试等场景。',
|
|
613
613
|
inputSchema: {
|
|
614
614
|
type: 'object',
|
|
615
615
|
properties: {
|
|
@@ -636,15 +636,50 @@ async function main() {
|
|
|
636
636
|
body: {
|
|
637
637
|
description: '请求体。传入对象时自动序列化为 JSON 并设置 Content-Type: application/json;传入字符串时原样发送',
|
|
638
638
|
},
|
|
639
|
+
formData: {
|
|
640
|
+
type: 'object',
|
|
641
|
+
description: 'multipart/form-data 文本字段。字段值支持 string、number、boolean;不能与 body 同时使用',
|
|
642
|
+
additionalProperties: {
|
|
643
|
+
anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
files: {
|
|
647
|
+
type: 'array',
|
|
648
|
+
description: 'multipart 文件列表,最多 20 个且总计不超过 40 MiB。本地调用优先传绝对 filePath;远程调用可传 base64,并同时提供 filename,Base64 文件内容总计不超过 7 MiB',
|
|
649
|
+
maxItems: 20,
|
|
650
|
+
items: {
|
|
651
|
+
type: 'object',
|
|
652
|
+
properties: {
|
|
653
|
+
fieldName: {
|
|
654
|
+
type: 'string',
|
|
655
|
+
description: 'multipart 字段名,例如 file',
|
|
656
|
+
},
|
|
657
|
+
filePath: {
|
|
658
|
+
type: 'string',
|
|
659
|
+
description: 'Mearl native-host 所在机器上的绝对文件路径',
|
|
660
|
+
},
|
|
661
|
+
base64: {
|
|
662
|
+
type: 'string',
|
|
663
|
+
description: '文件内容的 Base64;与 filePath 二选一',
|
|
664
|
+
},
|
|
665
|
+
filename: {
|
|
666
|
+
type: 'string',
|
|
667
|
+
description: '上传文件名;使用 base64 时必填',
|
|
668
|
+
},
|
|
669
|
+
contentType: {
|
|
670
|
+
type: 'string',
|
|
671
|
+
description: '文件 MIME 类型;默认根据文件名推断',
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
required: ['fieldName'],
|
|
675
|
+
oneOf: [{ required: ['filePath'] }, { required: ['base64', 'filename'] }],
|
|
676
|
+
},
|
|
677
|
+
},
|
|
639
678
|
withCookies: {
|
|
640
679
|
type: 'boolean',
|
|
641
680
|
description: '是否自动携带浏览器中该域名的 Cookie,默认 true',
|
|
642
681
|
default: true,
|
|
643
682
|
},
|
|
644
|
-
cookieDomain: {
|
|
645
|
-
type: 'string',
|
|
646
|
-
description: '指定获取 Cookie 的域名(默认从 url 中自动提取域名)',
|
|
647
|
-
},
|
|
648
683
|
responseType: {
|
|
649
684
|
type: 'string',
|
|
650
685
|
enum: ['json', 'text', 'auto'],
|
|
@@ -1040,6 +1075,10 @@ async function main() {
|
|
|
1040
1075
|
description: '是否激活(前台显示)新标签页。默认 false(后台打开)',
|
|
1041
1076
|
default: false,
|
|
1042
1077
|
},
|
|
1078
|
+
openerTabId: {
|
|
1079
|
+
type: 'number',
|
|
1080
|
+
description: '来源标签页 ID。侧边栏对话继续任务时传入,用于把新开或复用的标签页关联到同一对话',
|
|
1081
|
+
},
|
|
1043
1082
|
closeExisting: {
|
|
1044
1083
|
type: 'boolean',
|
|
1045
1084
|
description: '打开前是否先关闭 Agent 受控的已有标签页。默认 false',
|
|
@@ -1142,7 +1181,7 @@ async function main() {
|
|
|
1142
1181
|
},
|
|
1143
1182
|
{
|
|
1144
1183
|
name: 'browser_page_click',
|
|
1145
|
-
description: '点击页面元素,默认内置观察:同一次调用内等待相关 DOM、XHR/Fetch/MTop 请求或导航稳定,返回 { action, observation }
|
|
1184
|
+
description: '点击页面元素,默认内置观察:同一次调用内等待相关 DOM、XHR/Fetch/MTop 请求或导航稳定,返回 { action, observation };点击直接打开新标签页时 observation.openedTabs 返回可继续操作的 tabId、URL、标题和加载状态。传 observe:false 仅执行裸点击。定位方式三选一:selector(CSS 或 @eN snapshot ref)、text(按可见文本匹配 accessibility name,可用 scope 限定子树)、point(视口坐标)。clickMode 默认 auto:可见页按设备模拟状态使用可信 mouse/touch,隐藏页不切换标签或还原窗口并自动使用 DOM fallback;checkbox/radio 使用带状态校验的 DOM 路径。可用 dom/mouse/touch 强制派发方式。响应返回实际 dispatchMode,可信输入还返回 pointerType。',
|
|
1146
1185
|
inputSchema: {
|
|
1147
1186
|
type: 'object',
|
|
1148
1187
|
properties: {
|
|
@@ -1795,10 +1834,11 @@ async function main() {
|
|
|
1795
1834
|
break;
|
|
1796
1835
|
}
|
|
1797
1836
|
case 'browser_tab_open': {
|
|
1798
|
-
const { url, active = false, closeExisting = false, reuse = 'never', match, restoreFocusOnClose = false, emulation, group, } = args;
|
|
1837
|
+
const { url, active = false, openerTabId, closeExisting = false, reuse = 'never', match, restoreFocusOnClose = false, emulation, group, } = args;
|
|
1799
1838
|
result = await client.tabOpen({
|
|
1800
1839
|
url,
|
|
1801
1840
|
active,
|
|
1841
|
+
openerTabId,
|
|
1802
1842
|
closeExisting,
|
|
1803
1843
|
reuse,
|
|
1804
1844
|
match,
|
package/dist/uninstall.d.ts
CHANGED
|
@@ -6,4 +6,31 @@
|
|
|
6
6
|
* npx @mearl/mcp-server --uninstall
|
|
7
7
|
* 或 mearl-mcp-uninstall (如果全局安装)
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { MCPClientConfig } from './mcp-clients.js';
|
|
11
|
+
import { uninstallNativeHost } from '@mearl/native-host';
|
|
12
|
+
export type MCPConfigCleanupResult = 'removed' | 'not-found' | 'failed';
|
|
13
|
+
export declare function cleanMCPConfig(configPath: string, clientConfig: MCPClientConfig): MCPConfigCleanupResult;
|
|
14
|
+
export interface MCPUninstallDependencies {
|
|
15
|
+
cleanConfig?: typeof cleanMCPConfig;
|
|
16
|
+
clients?: Record<string, MCPClientConfig>;
|
|
17
|
+
exists?: typeof existsSync;
|
|
18
|
+
globalInstallationDetected?: () => boolean;
|
|
19
|
+
log?: (message: string) => void;
|
|
20
|
+
logError?: (message: string) => void;
|
|
21
|
+
prompt?: (options: {
|
|
22
|
+
type: 'multiselect';
|
|
23
|
+
name: 'clients';
|
|
24
|
+
message: string;
|
|
25
|
+
choices: Array<{
|
|
26
|
+
title: string;
|
|
27
|
+
value: string;
|
|
28
|
+
}>;
|
|
29
|
+
instructions: boolean;
|
|
30
|
+
hint: string;
|
|
31
|
+
}) => Promise<{
|
|
32
|
+
clients?: string[];
|
|
33
|
+
}>;
|
|
34
|
+
removeNativeHost?: typeof uninstallNativeHost;
|
|
35
|
+
}
|
|
36
|
+
export declare function runUninstall(argv?: string[], dependencies?: MCPUninstallDependencies): Promise<boolean>;
|
package/dist/uninstall.js
CHANGED
|
@@ -6,47 +6,53 @@
|
|
|
6
6
|
* npx @mearl/mcp-server --uninstall
|
|
7
7
|
* 或 mearl-mcp-uninstall (如果全局安装)
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
10
|
+
import { platform } from 'os';
|
|
11
|
+
import { execFileSync } from 'child_process';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
13
14
|
import prompts from 'prompts';
|
|
14
|
-
import { HOST_NAME } from './constants.js';
|
|
15
15
|
import { MCP_CLIENTS } from './mcp-clients.js';
|
|
16
|
-
|
|
17
|
-
function getNativeHostDir() {
|
|
18
|
-
const os = platform();
|
|
19
|
-
if (os === 'darwin') {
|
|
20
|
-
return join(homedir(), 'Library/Application Support/Google/Chrome/NativeMessagingHosts');
|
|
21
|
-
}
|
|
22
|
-
else if (os === 'linux') {
|
|
23
|
-
return join(homedir(), '.config/google-chrome/NativeMessagingHosts');
|
|
24
|
-
}
|
|
25
|
-
else if (os === 'win32') {
|
|
26
|
-
throw new Error('Windows 需要手动删除注册表项,请参考 README.md');
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
throw new Error(`不支持的平台: ${os}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
16
|
+
import { uninstallNativeHost } from '@mearl/native-host';
|
|
32
17
|
// 检查是否全局安装
|
|
33
18
|
function isGloballyInstalled() {
|
|
34
19
|
try {
|
|
35
20
|
const cmd = platform() === 'win32' ? 'where' : 'which';
|
|
36
|
-
|
|
21
|
+
execFileSync(cmd, ['mearl-mcp-server'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
37
22
|
return true;
|
|
38
23
|
}
|
|
39
24
|
catch {
|
|
40
25
|
return false;
|
|
41
26
|
}
|
|
42
27
|
}
|
|
28
|
+
function removeTomlMCPSection(content) {
|
|
29
|
+
const lines = content.split('\n');
|
|
30
|
+
const sectionStart = lines.findIndex(line => line.trim() === '[mcp_servers.mearl]');
|
|
31
|
+
if (sectionStart === -1)
|
|
32
|
+
return null;
|
|
33
|
+
let sectionEnd = lines.length;
|
|
34
|
+
for (let index = sectionStart + 1; index < lines.length; index += 1) {
|
|
35
|
+
if (lines[index].trim().startsWith('[')) {
|
|
36
|
+
sectionEnd = index;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [...lines.slice(0, sectionStart), ...lines.slice(sectionEnd)].join('\n');
|
|
41
|
+
}
|
|
43
42
|
// 清理 MCP 配置文件中的 mearl
|
|
44
|
-
function cleanMCPConfig(configPath, clientConfig) {
|
|
43
|
+
export function cleanMCPConfig(configPath, clientConfig) {
|
|
45
44
|
try {
|
|
46
45
|
if (!existsSync(configPath)) {
|
|
47
|
-
return
|
|
46
|
+
return 'not-found';
|
|
48
47
|
}
|
|
49
48
|
const content = readFileSync(configPath, 'utf-8');
|
|
49
|
+
if (clientConfig.useTomlFormat) {
|
|
50
|
+
const updated = removeTomlMCPSection(content);
|
|
51
|
+
if (updated === null)
|
|
52
|
+
return 'not-found';
|
|
53
|
+
writeFileSync(configPath, updated);
|
|
54
|
+
return 'removed';
|
|
55
|
+
}
|
|
50
56
|
const config = JSON.parse(content);
|
|
51
57
|
const useServersFormat = clientConfig.useServersFormat;
|
|
52
58
|
let hasConfig = false;
|
|
@@ -65,88 +71,103 @@ function cleanMCPConfig(configPath, clientConfig) {
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
if (!hasConfig) {
|
|
68
|
-
return
|
|
74
|
+
return 'not-found';
|
|
69
75
|
}
|
|
70
76
|
// 写回文件
|
|
71
77
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
72
|
-
return
|
|
78
|
+
return 'removed';
|
|
73
79
|
}
|
|
74
80
|
catch (error) {
|
|
75
81
|
console.error(` ⚠️ 清理配置失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
-
return
|
|
82
|
+
return 'failed';
|
|
77
83
|
}
|
|
78
84
|
}
|
|
79
85
|
// 卸载主函数
|
|
80
|
-
async function
|
|
81
|
-
|
|
86
|
+
export async function runUninstall(argv = process.argv.slice(2), dependencies = {}) {
|
|
87
|
+
const log = dependencies.log ?? (message => console.log(message));
|
|
88
|
+
const logError = dependencies.logError ?? (message => console.error(message));
|
|
89
|
+
const removeNativeHost = dependencies.removeNativeHost ?? uninstallNativeHost;
|
|
90
|
+
const detectGlobalInstallation = dependencies.globalInstallationDetected ?? isGloballyInstalled;
|
|
91
|
+
const fileExists = dependencies.exists ?? existsSync;
|
|
92
|
+
const clients = dependencies.clients ?? MCP_CLIENTS;
|
|
93
|
+
const prompt = dependencies.prompt ?? prompts;
|
|
94
|
+
const cleanConfig = dependencies.cleanConfig ?? cleanMCPConfig;
|
|
95
|
+
const managedBySetup = argv.includes('--managed-by-setup');
|
|
96
|
+
const yes = argv.includes('--yes') || argv.includes('-y');
|
|
97
|
+
log('🗑️ Mearl MCP 卸载\n');
|
|
82
98
|
try {
|
|
99
|
+
let completed = true;
|
|
83
100
|
// 1. 清理 Native Messaging Host 配置
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
let hasDeleted = false;
|
|
89
|
-
if (existsSync(manifestPath)) {
|
|
90
|
-
unlinkSync(manifestPath);
|
|
91
|
-
console.log(`✅ 删除配置文件`);
|
|
92
|
-
hasDeleted = true;
|
|
93
|
-
}
|
|
94
|
-
if (existsSync(wrapperPath)) {
|
|
95
|
-
unlinkSync(wrapperPath);
|
|
96
|
-
hasDeleted = true;
|
|
97
|
-
}
|
|
98
|
-
if (!hasDeleted) {
|
|
99
|
-
console.log('未找到配置文件');
|
|
100
|
-
}
|
|
101
|
+
log('清理 Native Messaging Host...');
|
|
102
|
+
const nativeHostRemoved = await removeNativeHost({ mode: 'mcp', silent: true });
|
|
103
|
+
log(nativeHostRemoved ? '✅ Native Messaging Host 已清理' : '⚠️ Native Messaging Host 清理失败');
|
|
104
|
+
completed = nativeHostRemoved;
|
|
101
105
|
// 2. 检查全局安装
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
if (!managedBySetup) {
|
|
107
|
+
log('\n检查 npm 全局安装...');
|
|
108
|
+
if (detectGlobalInstallation()) {
|
|
109
|
+
log('⚠️ 请手动卸载: npm uninstall -g @mearl/mcp-server');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
log('✅ 未检测到全局安装');
|
|
113
|
+
}
|
|
108
114
|
}
|
|
109
115
|
// 3. 清理 MCP 客户端配置
|
|
110
|
-
|
|
116
|
+
log('\n清理 MCP 客户端配置...');
|
|
111
117
|
// 检测哪些客户端有配置文件
|
|
112
|
-
const availableClients = Object.entries(
|
|
113
|
-
.filter(([_, client]) =>
|
|
118
|
+
const availableClients = Object.entries(clients)
|
|
119
|
+
.filter(([_, client]) => fileExists(client.configPath))
|
|
114
120
|
.map(([key, client]) => ({
|
|
115
121
|
title: client.name,
|
|
116
122
|
value: key,
|
|
117
123
|
}));
|
|
118
124
|
if (availableClients.length === 0) {
|
|
119
|
-
|
|
125
|
+
log('未找到 MCP 配置');
|
|
120
126
|
}
|
|
121
127
|
else {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
const selectedClients = yes
|
|
129
|
+
? availableClients.map(client => client.value)
|
|
130
|
+
: (await prompt({
|
|
131
|
+
type: 'multiselect',
|
|
132
|
+
name: 'clients',
|
|
133
|
+
message: '选择要清理的客户端:',
|
|
134
|
+
choices: availableClients,
|
|
135
|
+
instructions: false,
|
|
136
|
+
hint: '空格选择,回车确认',
|
|
137
|
+
})).clients;
|
|
138
|
+
if (selectedClients && selectedClients.length > 0) {
|
|
139
|
+
for (const clientKey of selectedClients) {
|
|
140
|
+
const client = clients[clientKey];
|
|
141
|
+
const result = cleanConfig(client.configPath, client);
|
|
142
|
+
if (result === 'removed') {
|
|
143
|
+
log(`✅ 已清理 ${client.name}`);
|
|
144
|
+
}
|
|
145
|
+
else if (result === 'not-found') {
|
|
146
|
+
log(`未找到 ${client.name} 配置`);
|
|
135
147
|
}
|
|
136
148
|
else {
|
|
137
|
-
|
|
149
|
+
completed = false;
|
|
138
150
|
}
|
|
139
151
|
}
|
|
140
152
|
}
|
|
141
153
|
else {
|
|
142
|
-
|
|
154
|
+
log('已取消');
|
|
143
155
|
}
|
|
144
156
|
}
|
|
145
|
-
|
|
157
|
+
if (completed) {
|
|
158
|
+
log('\n✅ 卸载完成!');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
logError('\n❌ 卸载未完整完成,请检查上述错误');
|
|
162
|
+
}
|
|
163
|
+
return completed;
|
|
146
164
|
}
|
|
147
165
|
catch (error) {
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
logError(`\n❌ 卸载失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
167
|
+
return false;
|
|
150
168
|
}
|
|
151
169
|
}
|
|
152
|
-
|
|
170
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
171
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
|
|
172
|
+
process.exitCode = (await runUninstall()) ? 0 : 1;
|
|
173
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/mcp-server",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "MCP Server for Mearl - enables AI to interact with Chrome browser requests and logs",
|
|
5
5
|
"mcpName": "io.github.F-loat/mearl",
|
|
6
6
|
"type": "module",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
32
32
|
"prompts": "^2.4.2",
|
|
33
|
-
"@mearl/client": "2.2.
|
|
34
|
-
"@mearl/native-host": "2.2.
|
|
33
|
+
"@mearl/client": "2.2.2",
|
|
34
|
+
"@mearl/native-host": "2.2.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^24.9.1",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"dev": "tsc --watch",
|
|
44
44
|
"start": "node dist/index.js",
|
|
45
45
|
"setup": "pnpm build && npm link",
|
|
46
|
+
"test": "vitest run",
|
|
46
47
|
"typecheck": "tsc --noEmit"
|
|
47
48
|
}
|
|
48
49
|
}
|