@attson/atwebpilot-mcp 0.0.74 → 0.0.75
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 +11 -9
- package/dist/index.js +124 -39
- package/dist/skill/SKILL.md +261 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ Claude Code:
|
|
|
17
17
|
**不需要手填端口。** 从 Plan 33 起是配对流程:
|
|
18
18
|
|
|
19
19
|
1. 会话启动时**不绑任何端口** —— 不碰网页的会话零副作用。
|
|
20
|
-
2. AI 第一次调 `list_tabs` / `
|
|
21
|
-
3.
|
|
20
|
+
2. AI 第一次调 `list_tabs` / `pairing_status` 时才绑一个空闲端口,尝试用系统默认浏览器打开配对页,并立即返回 `pairing_required + pair_url`,不阻塞等待。
|
|
21
|
+
3. 你在浏览器里点「允许」,扩展即接入;AI 用轻量 `pairing_status` 确认连接后再读取 tab,不重复阻塞式调用。
|
|
22
22
|
4. 之后本机的会话都免确认(信任是安装级的);端口能复用时连配对页都不会弹。
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
若系统未弹出浏览器,可直接打开返回结果中的 `pair_url`。授权成功只以扩展实际建立连接并发送 `HELLO` 为准;打开配对页本身不代表已连接。
|
|
25
25
|
|
|
26
26
|
多个 Codex / Claude Code 会话可以同时接入同一个浏览器,各自一条连接。可在扩展设置 →
|
|
27
27
|
Coordinator 查看已接入的会话、单独断开,或撤销信任。
|
|
@@ -48,9 +48,9 @@ listen 端口的能力,所以方向只能如此。这也是配对页存在的
|
|
|
48
48
|
|
|
49
49
|
## 工具面
|
|
50
50
|
|
|
51
|
-
- 控制面
|
|
51
|
+
- 控制面 5 个:`pairing_status / list_tabs / open_session / close_session / get_quota`
|
|
52
52
|
- 发现 1 个:`browser_discoverTools` —— 已知工具时直接传 `enable: [...]`,已知分组时直接传 `enableGroups: ["export", ...]`;只有两者都未知时才不带参数读取目录。启用后会加入本进程的 `tools/list`(发送 `tools/list_changed`),并直接返回完整 schema。
|
|
53
|
-
-
|
|
53
|
+
- 执行面在浏览器连接前不进入 `tools/list`;连接确认后加载默认 **core 31 个** `browser_*`。其余 20 个由 AI 按需 `discoverTools` 拉取,用户不用配置。
|
|
54
54
|
- 不暴露 `askUser`(MCP 会话没有人在侧边栏应答)和 `attachTab` / `detachTab`(目标 tab 已由 `open_session` 绑定)。
|
|
55
55
|
|
|
56
56
|
MCP 层的描述是精简英文(≈2.5k tokens for core),侧边栏内置 LLM 仍用中文长描述;两者共享同一份 `TOOL_DEFS`,靠 `mcp` 字段区分。
|
|
@@ -65,8 +65,8 @@ hover / uploadFile / scroll / waitFor`);`browser_storage` 也不会列出,
|
|
|
65
65
|
|
|
66
66
|
| 值 | 效果 |
|
|
67
67
|
|---|---|
|
|
68
|
-
| `core`(默认) | 31 个核心工具 + `browser_discoverTools
|
|
69
|
-
| `full` |
|
|
68
|
+
| `core`(默认) | 连接前仅控制面;连接后 31 个核心工具 + `browser_discoverTools` |
|
|
69
|
+
| `full` | 连接前仍仅控制面;连接后列出全部 51 个工具 |
|
|
70
70
|
|
|
71
71
|
无法识别的值(包括已移除的 `parity`)按 `core` 处理,并往 stderr 打一条提示。
|
|
72
72
|
|
|
@@ -77,12 +77,14 @@ hover / uploadFile / scroll / waitFor`);`browser_storage` 也不会列出,
|
|
|
77
77
|
```bash
|
|
78
78
|
pnpm analyze:sessions
|
|
79
79
|
pnpm analyze:sessions -- --since 7d --clients claude --format json
|
|
80
|
+
pnpm analyze:sessions -- --since 24h --clients codex --latest-session
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
分析器只读扫描 `~/.claude/projects` 与 `~/.codex/sessions` 中最近 30 天的
|
|
83
|
-
JSONL,识别真实 AtWebPilot
|
|
84
|
+
JSONL,识别真实 AtWebPilot 工具调用及重复验证、固定等待、重复截图、可批量填表、配对超时与无状态重试等
|
|
84
85
|
模式。输出只包含聚合计数、参数字段名与不可逆指纹;不会输出或保存 prompt、参数值、
|
|
85
|
-
|
|
86
|
+
工具结果正文、凭证或绝对会话路径。Codex 嵌套调用会关联 wait cell 的耗时、错误以及
|
|
87
|
+
cached/uncached token;使用 `--latest-session` 只看最近一条真实使用记录。
|
|
86
88
|
|
|
87
89
|
### 从 0.0.70 及更早版本迁移
|
|
88
90
|
|
package/dist/index.js
CHANGED
|
@@ -946,14 +946,19 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprot
|
|
|
946
946
|
|
|
947
947
|
// src/control-tools.ts
|
|
948
948
|
var CONTROL_TOOLS = [
|
|
949
|
+
{
|
|
950
|
+
name: "pairing_status",
|
|
951
|
+
description: "Start or check browser pairing without waiting. Returns connected or pairing_required with the pair URL.",
|
|
952
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
953
|
+
},
|
|
949
954
|
{
|
|
950
955
|
name: "list_tabs",
|
|
951
|
-
description: "
|
|
956
|
+
description: "List browser tabs. If pairing is needed, opens the pair page and immediately returns pairing_required with its URL; wait for user approval, then call pairing_status instead of retrying this tool.",
|
|
952
957
|
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
953
958
|
},
|
|
954
959
|
{
|
|
955
960
|
name: "open_session",
|
|
956
|
-
description: "
|
|
961
|
+
description: "Open a session for a tab from list_tabs. Returns immediately with PAIRING_REQUIRED if the browser is not connected.",
|
|
957
962
|
inputSchema: {
|
|
958
963
|
type: "object",
|
|
959
964
|
required: ["tab_id"],
|
|
@@ -2280,16 +2285,46 @@ function locate(source, offset) {
|
|
|
2280
2285
|
}
|
|
2281
2286
|
|
|
2282
2287
|
// src/handlers.ts
|
|
2283
|
-
|
|
2288
|
+
function singleWorkerId(c, pairUrl) {
|
|
2289
|
+
const workers = c.workers.list();
|
|
2290
|
+
if (workers.length === 0) {
|
|
2291
|
+
throw new Error(
|
|
2292
|
+
"PAIRING_REQUIRED: \u6D4F\u89C8\u5668\u5C1A\u672A\u8FDE\u63A5\u3002" + (pairUrl ? `\u8BF7\u5728\u6D4F\u89C8\u5668\u4E2D\u5B8C\u6210\u914D\u5BF9\uFF1A${pairUrl}` : "\u8BF7\u5148\u8C03\u7528 pairing_status \u83B7\u53D6\u914D\u5BF9\u9875\u3002")
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
if (workers.length > 1) throw new Error("\u68C0\u6D4B\u5230\u591A\u4E2A\u6D4F\u89C8\u5668\u8FDE\u5165\uFF1Bv1 \u4EC5\u652F\u6301\u5355 worker\uFF0C\u8BF7\u53EA\u4FDD\u7559\u4E00\u4E2A\u8FDE\u63A5");
|
|
2296
|
+
return workers[0].id;
|
|
2297
|
+
}
|
|
2298
|
+
async function handlePairingStatus(deps) {
|
|
2284
2299
|
const { coordinator } = await deps.ensure();
|
|
2285
|
-
const
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2300
|
+
const workers = coordinator.workers.list();
|
|
2301
|
+
if (workers.length > 1) {
|
|
2302
|
+
throw new Error("\u68C0\u6D4B\u5230\u591A\u4E2A\u6D4F\u89C8\u5668\u8FDE\u5165\uFF1Bv1 \u4EC5\u652F\u6301\u5355 worker\uFF0C\u8BF7\u53EA\u4FDD\u7559\u4E00\u4E2A\u8FDE\u63A5");
|
|
2303
|
+
}
|
|
2304
|
+
if (workers.length === 1) {
|
|
2305
|
+
return { status: "connected", tab_count: workers[0].available_tabs.length };
|
|
2306
|
+
}
|
|
2307
|
+
const pairUrl = deps.pairUrl();
|
|
2308
|
+
if (!pairUrl) throw new Error("PAIRING_REQUIRED: \u914D\u5BF9\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u4F46\u914D\u5BF9\u9875\u5C1A\u4E0D\u53EF\u7528");
|
|
2309
|
+
return { status: "pairing_required", pair_url: pairUrl };
|
|
2289
2310
|
}
|
|
2290
|
-
async function
|
|
2311
|
+
async function handleListTabs(deps) {
|
|
2291
2312
|
const { coordinator } = await deps.ensure();
|
|
2292
|
-
const
|
|
2313
|
+
const workers = coordinator.workers.list();
|
|
2314
|
+
if (workers.length > 1) {
|
|
2315
|
+
throw new Error("\u68C0\u6D4B\u5230\u591A\u4E2A\u6D4F\u89C8\u5668\u8FDE\u5165\uFF1Bv1 \u4EC5\u652F\u6301\u5355 worker\uFF0C\u8BF7\u53EA\u4FDD\u7559\u4E00\u4E2A\u8FDE\u63A5");
|
|
2316
|
+
}
|
|
2317
|
+
const worker = workers[0];
|
|
2318
|
+
if (!worker) {
|
|
2319
|
+
const pairUrl = deps.pairUrl();
|
|
2320
|
+
if (!pairUrl) throw new Error("PAIRING_REQUIRED: \u914D\u5BF9\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u4F46\u914D\u5BF9\u9875\u5C1A\u4E0D\u53EF\u7528");
|
|
2321
|
+
return { status: "pairing_required", pair_url: pairUrl, tabs: [] };
|
|
2322
|
+
}
|
|
2323
|
+
return { status: "connected", tabs: worker.available_tabs };
|
|
2324
|
+
}
|
|
2325
|
+
async function handleOpenSession(deps, args) {
|
|
2326
|
+
const { coordinator } = await deps.ensure();
|
|
2327
|
+
const worker_id = singleWorkerId(coordinator, deps.pairUrl());
|
|
2293
2328
|
const tab_id = String(args.tab_id);
|
|
2294
2329
|
const requested = Array.isArray(args.capabilities) ? args.capabilities.map(String).filter(isCapability) : [];
|
|
2295
2330
|
const scope = new Set(requested.length ? requested : CAPABILITIES);
|
|
@@ -2381,6 +2416,37 @@ import { dirname, join } from "path";
|
|
|
2381
2416
|
import { fileURLToPath } from "url";
|
|
2382
2417
|
var SKILL_NAME = "atwebpilot-browser";
|
|
2383
2418
|
var SKILL_DESCRIPTION = "Strategy + scenarios + safety rails for driving the AtWebPilot browser extension via MCP.";
|
|
2419
|
+
var TOPICS = {
|
|
2420
|
+
quickstart: `# AtWebPilot quickstart
|
|
2421
|
+
|
|
2422
|
+
1. Call list_tabs once. If it returns pairing_required, show pair_url and wait for the user to approve.
|
|
2423
|
+
2. After approval, call pairing_status. Do not retry list_tabs while status is unchanged.
|
|
2424
|
+
3. Call list_tabs when connected, choose a tab, then open_session with its tab_id.
|
|
2425
|
+
4. For interaction use browser_getPageInfo then browser_takeSnapshot; for broad reading use browser_createPageIndex.
|
|
2426
|
+
5. Prefer purpose-built tools and verified results. Use browser_discoverTools only for capabilities outside the connected core surface.
|
|
2427
|
+
6. Close the session when finished.
|
|
2428
|
+
`,
|
|
2429
|
+
pairing: `# AtWebPilot pairing
|
|
2430
|
+
|
|
2431
|
+
list_tabs and pairing_status never wait for browser authorization. A pairing_required result includes pair_url. Show that URL once and wait for the user to approve in the browser. Then call pairing_status; only call list_tabs after it reports connected. Never poll list_tabs or inspect the pair-page HTML.
|
|
2432
|
+
`,
|
|
2433
|
+
reading: `# AtWebPilot reading
|
|
2434
|
+
|
|
2435
|
+
Call browser_getPageInfo first. For articles, products, tables, and forms, use browser_createPageIndex, then browser_extractPageFields or browser_searchPageIndex, and read only the relevant block with browser_readPageBlock. Avoid browser_extractText on body and full-DOM reads. Use targeted screenshots only when visual evidence matters.
|
|
2436
|
+
`,
|
|
2437
|
+
interaction: `# AtWebPilot interaction
|
|
2438
|
+
|
|
2439
|
+
Refresh browser_takeSnapshot before each major action. Prefer uid-based browser_clickByUid/browser_fillByUid, and use browser_fillForm for independent fields. Use condition-based browser_waitFor rather than fixed delays. Keep submit, upload, storage, and credentialed network actions as explicit reviewed steps.
|
|
2440
|
+
`,
|
|
2441
|
+
visual: `# AtWebPilot visual validation
|
|
2442
|
+
|
|
2443
|
+
Use browser_resize and trust its actualViewport/verified result. Capture one browser_screenshot after the page settles; target a selector or page-index block and lower scale when detail permits. Do not follow browser_resize or browser_screenshot with browser_runJS that only re-reads viewport dimensions.
|
|
2444
|
+
`,
|
|
2445
|
+
debugging: `# AtWebPilot debugging
|
|
2446
|
+
|
|
2447
|
+
Read browser_consoleMessages first, then browser_networkRequests summaries. Arm recorder bodies only when needed, reproduce once, and fetch one browser_networkRequestDetail. Poll incrementally with sinceId. Check backend and degradedReason before treating missing events as proof.
|
|
2448
|
+
`
|
|
2449
|
+
};
|
|
2384
2450
|
var cached = null;
|
|
2385
2451
|
function locateSkillFile() {
|
|
2386
2452
|
let here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -2395,7 +2461,13 @@ function locateSkillFile() {
|
|
|
2395
2461
|
}
|
|
2396
2462
|
return join(dirname(fileURLToPath(import.meta.url)), "../../../skill/SKILL.md");
|
|
2397
2463
|
}
|
|
2398
|
-
function readSkillBundle() {
|
|
2464
|
+
function readSkillBundle(rawTopic = "quickstart") {
|
|
2465
|
+
const topic = rawTopic == null ? "quickstart" : String(rawTopic);
|
|
2466
|
+
if (topic !== "full") {
|
|
2467
|
+
const content = TOPICS[topic];
|
|
2468
|
+
if (!content) throw new Error(`Unknown skill topic: ${topic}`);
|
|
2469
|
+
return { name: SKILL_NAME, description: SKILL_DESCRIPTION, content };
|
|
2470
|
+
}
|
|
2399
2471
|
if (cached == null) {
|
|
2400
2472
|
try {
|
|
2401
2473
|
cached = readFileSync(locateSkillFile(), "utf-8");
|
|
@@ -2412,15 +2484,29 @@ ${SKILL_DESCRIPTION}
|
|
|
2412
2484
|
}
|
|
2413
2485
|
var SKILL_TOOL = {
|
|
2414
2486
|
name: "atwebpilot_skill_read",
|
|
2415
|
-
description: "Return
|
|
2416
|
-
inputSchema: {
|
|
2487
|
+
description: "Return concise AtWebPilot guidance by topic. Defaults to quickstart; request full only when detailed reference is necessary.",
|
|
2488
|
+
inputSchema: {
|
|
2489
|
+
type: "object",
|
|
2490
|
+
properties: {
|
|
2491
|
+
topic: {
|
|
2492
|
+
type: "string",
|
|
2493
|
+
enum: ["quickstart", "pairing", "reading", "interaction", "visual", "debugging", "full"],
|
|
2494
|
+
description: "Guidance section; quickstart is the token-efficient default"
|
|
2495
|
+
}
|
|
2496
|
+
},
|
|
2497
|
+
required: [],
|
|
2498
|
+
additionalProperties: false
|
|
2499
|
+
}
|
|
2417
2500
|
};
|
|
2418
2501
|
|
|
2419
2502
|
// src/mcp-server.ts
|
|
2420
2503
|
var ALL_BROWSER_TOOLS = generateBrowserTools("full");
|
|
2421
2504
|
var BROWSER_BY_NAME = new Map(ALL_BROWSER_TOOLS.map((t) => [t.name, t]));
|
|
2422
2505
|
function createToolState(mode) {
|
|
2423
|
-
return {
|
|
2506
|
+
return {
|
|
2507
|
+
advertised: new Set(generateBrowserTools(mode).map((t) => t.name)),
|
|
2508
|
+
browserSurfaceVisible: false
|
|
2509
|
+
};
|
|
2424
2510
|
}
|
|
2425
2511
|
var DEFAULT_STATE = createToolState(readToolMode(process.env));
|
|
2426
2512
|
var LEGACY_TOOLS = [
|
|
@@ -2454,7 +2540,7 @@ function workerToolSupport(deps) {
|
|
|
2454
2540
|
}
|
|
2455
2541
|
function buildToolList(deps, state = DEFAULT_STATE) {
|
|
2456
2542
|
const supported = workerToolSupport(deps);
|
|
2457
|
-
const browser = ALL_BROWSER_TOOLS.filter((t) => state.advertised.has(t.name)).filter((t) => !supported || t.builtinTools.every((b) => supported.has(b)));
|
|
2543
|
+
const browser = state.browserSurfaceVisible || supported ? ALL_BROWSER_TOOLS.filter((t) => state.advertised.has(t.name)).filter((t) => !supported || t.builtinTools.every((b) => supported.has(b))) : [];
|
|
2458
2544
|
return [
|
|
2459
2545
|
{ name: SKILL_TOOL.name, description: SKILL_TOOL.description, inputSchema: SKILL_TOOL.inputSchema },
|
|
2460
2546
|
...CONTROL_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
@@ -2478,10 +2564,10 @@ function toolResult(gen, data) {
|
|
|
2478
2564
|
return { content };
|
|
2479
2565
|
}
|
|
2480
2566
|
var fail2 = (message) => ({ content: [{ type: "text", text: message }], isError: true });
|
|
2481
|
-
async function dispatchCall(deps, name, args,
|
|
2567
|
+
async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChanged) {
|
|
2482
2568
|
try {
|
|
2483
2569
|
if (name === SKILL_TOOL.name) {
|
|
2484
|
-
const bundle = readSkillBundle();
|
|
2570
|
+
const bundle = readSkillBundle(args.topic);
|
|
2485
2571
|
return { content: [{ type: "text", text: bundle.content }] };
|
|
2486
2572
|
}
|
|
2487
2573
|
if (name === DISCOVER_TOOL.name) {
|
|
@@ -2495,8 +2581,21 @@ async function dispatchCall(deps, name, args, onPairingRequired, state = DEFAULT
|
|
|
2495
2581
|
const { changed: _c, ...body } = r;
|
|
2496
2582
|
return ok(body);
|
|
2497
2583
|
}
|
|
2498
|
-
if (name === "
|
|
2499
|
-
|
|
2584
|
+
if (name === "pairing_status") {
|
|
2585
|
+
const result = await handlePairingStatus(deps);
|
|
2586
|
+
if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
|
|
2587
|
+
return ok(result);
|
|
2588
|
+
}
|
|
2589
|
+
if (name === "list_tabs") {
|
|
2590
|
+
const result = await handleListTabs(deps);
|
|
2591
|
+
if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
|
|
2592
|
+
return ok(result);
|
|
2593
|
+
}
|
|
2594
|
+
if (name === "open_session") {
|
|
2595
|
+
const result = await handleOpenSession(deps, args);
|
|
2596
|
+
await revealBrowserSurface(state, onListChanged);
|
|
2597
|
+
return ok(result);
|
|
2598
|
+
}
|
|
2500
2599
|
if (name === "close_session") return ok(await handleCloseSession(deps, args));
|
|
2501
2600
|
if (name === "get_quota") return ok(await handleGetQuota(deps, args));
|
|
2502
2601
|
const gen = BROWSER_BY_NAME.get(name);
|
|
@@ -2506,33 +2605,19 @@ async function dispatchCall(deps, name, args, onPairingRequired, state = DEFAULT
|
|
|
2506
2605
|
return fail2(e instanceof Error ? e.message : String(e));
|
|
2507
2606
|
}
|
|
2508
2607
|
}
|
|
2608
|
+
async function revealBrowserSurface(state, onListChanged) {
|
|
2609
|
+
if (state.browserSurfaceVisible) return;
|
|
2610
|
+
state.browserSurfaceVisible = true;
|
|
2611
|
+
await onListChanged?.();
|
|
2612
|
+
}
|
|
2509
2613
|
function createMcpServer(deps, state = DEFAULT_STATE) {
|
|
2510
2614
|
const server = new Server(
|
|
2511
2615
|
{ name: "atwebpilot-mcp", version: "0.1.0" },
|
|
2512
2616
|
{ capabilities: { tools: { listChanged: true }, logging: {} } }
|
|
2513
2617
|
);
|
|
2514
2618
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList(deps, state) }));
|
|
2515
|
-
server.setRequestHandler(CallToolRequestSchema, async (req
|
|
2619
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
2516
2620
|
const args = req.params.arguments ?? {};
|
|
2517
|
-
const onPairingRequired = async (url) => {
|
|
2518
|
-
const message = `\u7B49\u5F85\u6D4F\u89C8\u5668\u6388\u6743\uFF08\u6700\u591A 90 \u79D2\uFF09\u3002\u914D\u5BF9\u9875\uFF1A${url}\u3002\u5DF2\u5C1D\u8BD5\u81EA\u52A8\u6253\u5F00\uFF1B\u82E5\u672A\u5F39\u51FA\uFF0C\u8BF7\u624B\u52A8\u6253\u5F00\u3002`;
|
|
2519
|
-
try {
|
|
2520
|
-
const progressToken = extra._meta?.progressToken;
|
|
2521
|
-
if (progressToken != null) {
|
|
2522
|
-
await extra.sendNotification({
|
|
2523
|
-
method: "notifications/progress",
|
|
2524
|
-
params: { progressToken, progress: 0, total: 1, message }
|
|
2525
|
-
});
|
|
2526
|
-
} else {
|
|
2527
|
-
await server.sendLoggingMessage({ level: "info", logger: "atwebpilot", data: message });
|
|
2528
|
-
}
|
|
2529
|
-
} catch (error) {
|
|
2530
|
-
console.error(
|
|
2531
|
-
"[atwebpilot-mcp] failed to report pairing URL:",
|
|
2532
|
-
error instanceof Error ? error.message : String(error)
|
|
2533
|
-
);
|
|
2534
|
-
}
|
|
2535
|
-
};
|
|
2536
2621
|
const onListChanged = async () => {
|
|
2537
2622
|
try {
|
|
2538
2623
|
await server.sendToolListChanged();
|
|
@@ -2540,7 +2625,7 @@ function createMcpServer(deps, state = DEFAULT_STATE) {
|
|
|
2540
2625
|
console.error("[atwebpilot-mcp] failed to send tools/list_changed:", error instanceof Error ? error.message : String(error));
|
|
2541
2626
|
}
|
|
2542
2627
|
};
|
|
2543
|
-
return dispatchCall(deps, req.params.name, args,
|
|
2628
|
+
return dispatchCall(deps, req.params.name, args, state, onListChanged);
|
|
2544
2629
|
});
|
|
2545
2630
|
return server;
|
|
2546
2631
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# atwebpilot-browser skill
|
|
2
|
+
|
|
3
|
+
A reusable strategy for agents that drive AtWebPilot's browser extension via
|
|
4
|
+
MCP. Inspired by AIPex's [aipex-browser](https://github.com/AIPexStudio/AIPex)
|
|
5
|
+
pattern: tool usage flow, common scenarios, safety rails — all in one bundle
|
|
6
|
+
so an agent doesn't have to discover them from scratch.
|
|
7
|
+
|
|
8
|
+
## Capabilities you have
|
|
9
|
+
|
|
10
|
+
When this skill is loaded, you can drive any open tab through these tools
|
|
11
|
+
(exposed by `@attson/atwebpilot-mcp`):
|
|
12
|
+
|
|
13
|
+
### Control plane
|
|
14
|
+
|
|
15
|
+
- `pairing_status` — start/check pairing without waiting. When it reports
|
|
16
|
+
`pairing_required`, show `pair_url` once and wait for browser approval.
|
|
17
|
+
- `list_tabs` — enumerate Chrome tabs you can operate on. Entries carry `busy`
|
|
18
|
+
and `busy_label` when **another** Claude Code session is already driving that
|
|
19
|
+
tab, and `mine` for tabs you hold. This is advisory — nothing blocks you — but
|
|
20
|
+
contending for a tab another agent is typing into is rarely what you want.
|
|
21
|
+
Open your own with `browser_openTab` instead.
|
|
22
|
+
- `open_session(tabId)` — pin a tab so subsequent calls target it
|
|
23
|
+
- `close_session(sessionId)` — release the tab
|
|
24
|
+
- `get_quota(sessionId)` — see remaining requests this minute
|
|
25
|
+
|
|
26
|
+
### Browser-side built-ins (per session)
|
|
27
|
+
|
|
28
|
+
All of these are prefixed `browser_` over MCP, e.g. `browser_takeSnapshot`.
|
|
29
|
+
|
|
30
|
+
**Core — advertised after the browser connects (31):**
|
|
31
|
+
|
|
32
|
+
| Class | Tools |
|
|
33
|
+
|---|---|
|
|
34
|
+
| page state(safe) | `takeSnapshot`, `findElements`, `getPageInfo`, `extractText` |
|
|
35
|
+
| page index(safe) | `createPageIndex`, `searchPageIndex`, `readPageBlock`, `extractPageFields` |
|
|
36
|
+
| interaction(caution) | `clickByUid`, `click`, `fillByUid`, `fillInput`, `fillForm`, `selectOption`, `setCheckbox`, `hover`, `pressKey`, `drag`, `drop`(files ⇒ dangerous), `uploadFile`(dangerous) |
|
|
37
|
+
| navigation / tabs | `navigate`(`action` 取 `back` / `forward` / `reload` / `goto`), `listTabs`, `openTab`, `closeTab`, `resize`, `scroll` |
|
|
38
|
+
| observation | `screenshot`, `waitFor`, `runJS`(static-scanned), `consoleMessages`, `networkRequests` |
|
|
39
|
+
|
|
40
|
+
**Discoverable — enable directly with `browser_discoverTools`:**
|
|
41
|
+
|
|
42
|
+
| Group | Tools |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `export` | `downloadImage`, `downloadSpreadsheet`(real `.xlsx`, multi-sheet) |
|
|
45
|
+
| `network` | `httpRequest`(`withCredentials` ⇒ dangerous), `networkRequestDetail`(dangerous), `recorderConfig`, `handleDialog` |
|
|
46
|
+
| `storage` | `storage`(`op` 取 `get` / `set`; dangerous) |
|
|
47
|
+
| `browser-data` | `searchBookmarks`, `searchHistory` |
|
|
48
|
+
| `inspect` | `inspectElement`, `highlight`(`text` or `selector`/`uid`), `getValue`, `extractFormState` |
|
|
49
|
+
| `legacy-dom` | `snapshotDOM`, `querySelector`, `querySelectorAll`, `extractImages`, `focus` |
|
|
50
|
+
| `form` | `submitForm`(dangerous) |
|
|
51
|
+
| `tabs` | `switchToTab`(只在用户明确要求把 Chrome 切到前台时使用) |
|
|
52
|
+
|
|
53
|
+
`askUser`, `attachTab` and `detachTab` are **not** exposed: an MCP session has
|
|
54
|
+
no human at the side panel, and its tab is already bound by `open_session`.
|
|
55
|
+
|
|
56
|
+
### Getting more tools
|
|
57
|
+
|
|
58
|
+
The default list is deliberately small. When a task needs something outside
|
|
59
|
+
core — exporting a spreadsheet, calling an API with cookies, reading
|
|
60
|
+
localStorage, inspecting a request body, searching history, debugging layout —
|
|
61
|
+
do this, in order:
|
|
62
|
+
|
|
63
|
+
1. If you know the tool, enable it directly with
|
|
64
|
+
`browser_discoverTools({ enable: ["browser_downloadSpreadsheet", ...] })`.
|
|
65
|
+
If you know only the group, use
|
|
66
|
+
`browser_discoverTools({ enableGroups: ["export"] })`.
|
|
67
|
+
2. Call `browser_discoverTools({})` only when neither the tool nor group is
|
|
68
|
+
known. Enabled tools join `tools/list` (a `tools/list_changed` notification
|
|
69
|
+
is sent), and the response carries their full schemas for immediate use.
|
|
70
|
+
|
|
71
|
+
**Do not** rebuild these capabilities with `runJS` (`fetch`, CSV blobs,
|
|
72
|
+
`localStorage[...]`). It is slower, loses schema validation, and usually trips
|
|
73
|
+
the dangerous-tier review that the purpose-built tool would have avoided.
|
|
74
|
+
|
|
75
|
+
`ATWEBPILOT_MCP_TOOLS=full` advertises everything from the start for users who
|
|
76
|
+
prefer to pay the context cost once.
|
|
77
|
+
|
|
78
|
+
## Recommended flow
|
|
79
|
+
|
|
80
|
+
1. **先建立连接**:调一次 `list_tabs`;若返回 `pairing_required`,展示 `pair_url` 并等待用户批准。批准后只调 `pairing_status`,它报告 `connected` 后再调 `list_tabs`,不要轮询 `list_tabs`。
|
|
81
|
+
2. **探查先于操作**:每次进入新页面,先 `getPageInfo` 确认位置,再 `takeSnapshot`(要点击/填表)或 `createPageIndex`(要读内容/抽字段);`snapshotDOM` / `querySelector` 属于 legacy-dom 组,只在需要分析整页结构时通过 `browser_discoverTools` 启用。
|
|
82
|
+
3. **最少充分验证**:危险或不可逆操作保持单步;多个独立字段用 `fillForm`。工具已返回 `verified` 或明确后置状态时,不要再用 `runJS` 重复验证。
|
|
83
|
+
4. **dangerous 工具会被人工审核**:调用前用 `extractText` 给用户看上下文,让审批更顺。
|
|
84
|
+
5. **跑不动了就停下来问**:候选不唯一、缺关键信息、需要二次确认时,把问题写在回复里交给用户,不要瞎猜(MCP 会话没有 `askUser`)。
|
|
85
|
+
6. **完成后给一个简洁的总结**:用户希望看到「做了 N 步,最终结果 X」,不希望看流水账。
|
|
86
|
+
|
|
87
|
+
## Tool usage notes (moved here from the tool descriptions)
|
|
88
|
+
|
|
89
|
+
- `snapshotDOM`: `{}` (maxDepth 3) / `{ root: '.main', maxDepth: 5 }` / `{ maxDepth: 8 }`. Prefer `takeSnapshot` when you will click or fill afterwards.
|
|
90
|
+
- `querySelector` / `querySelectorAll`: `{ selector: 'button[type=submit]' }`, `{ selector: '.comment-item', limit: 50 }`.
|
|
91
|
+
- `extractText`: `{ selector: 'h1', single: true }`, `{ selector: 'article p' }`. Never `body` — use `createPageIndex`.
|
|
92
|
+
- `extractImages`: `{}` for the page, `{ root: '.product-gallery' }` for a region.
|
|
93
|
+
- `scroll`: `{ to: 'bottom', max: 5 }` for lazy loading; `{ to: 'bottom', max: 10, untilSelector: '.item:nth-child(20)' }` to stop once content appears.
|
|
94
|
+
- `waitFor`: `{ ms: 500 }` or `{ selector: '.lazy-loaded', timeoutMs: 8000 }`.
|
|
95
|
+
- `fillInput`: `{ selector: 'input[name=email]', value: 'a@b.c' }`; `clear: false` appends.
|
|
96
|
+
- `fillForm`: `{ fields: [{ selector: 'input[name=name]', value: '张三' }, { uid: 'el_5', value: 'mushroom' }] }` — much cheaper than repeated `fillInput`.
|
|
97
|
+
- `pressKey`: `{ selector: 'input[name=q]', key: 'Enter' }` submits a form-less search; `{ key: 'Escape' }` closes a modal.
|
|
98
|
+
- `navigate`: `{ action: 'back' }`, `{ action: 'goto', url: 'https://example.com/page' }`.
|
|
99
|
+
- `switchToTab`: all tools already target the session-bound tab. Discover and call this only when the user explicitly asks to bring Chrome and that tab to the foreground; never use it as setup for click, wait, runJS, or screenshot.
|
|
100
|
+
- `resize`: returns measured `actualViewport`, `devicePixelRatio`, and `verified`; do not follow it with a viewport-only `runJS` probe.
|
|
101
|
+
- `screenshot`: the image block is followed by compact capture metadata (backend, dimensions and target when available); use that instead of probing the viewport again.
|
|
102
|
+
- `httpRequest`: `{ url: '.../api/comments?page=2' }` (no cookies) vs `{ url, withCredentials: true }` (cookied, reviewed).
|
|
103
|
+
- `consoleMessages`: `{ level: 'error', limit: 50 }`; incremental polling with `{ sinceId }`.
|
|
104
|
+
- `networkRequestDetail`: arm bodies first with `recorderConfig({ bodies: true })` in main-world mode.
|
|
105
|
+
- `drop`: `{ selector: '#dropzone', files: [{ name: 'a.csv', mimeType: 'text/csv', base64: '...' }] }`.
|
|
106
|
+
- `downloadSpreadsheet`: `{ filename: 'items', sheets: [{ name: 'Sheet1', rows: [{ title: 'A', price: 12 }], columns: [{ key: 'title', header: '标题' }, { key: 'price' }] }] }`.
|
|
107
|
+
- `storage`: `{ op: 'get', store: 'local', key: 'token' }`, `{ op: 'set', store: 'session', key: 'k', value: '{"a":1}' }`.
|
|
108
|
+
- `highlight`: `{ text: 'Checkout' }` or `{ selector: '#pay' }` / `{ uid: 'el_3' }`; visual only, 3 s by default.
|
|
109
|
+
|
|
110
|
+
## Scenarios
|
|
111
|
+
|
|
112
|
+
### 总结此页
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
createPageIndex({})
|
|
116
|
+
extractText({ selector: "main, article, .content" })
|
|
117
|
+
→ 文本总结
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 填表 + 提交
|
|
121
|
+
|
|
122
|
+
`submitForm` 属于 form 组,先 `browser_discoverTools({ enable: ["browser_submitForm"] })` 启用。
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
takeSnapshot() // 找输入框
|
|
126
|
+
fillInput({ selector: "#name", value: "张三" })
|
|
127
|
+
setCheckbox({ selector: "#agree", checked: true })
|
|
128
|
+
selectOption({ selector: "#city", value: "北京" })
|
|
129
|
+
→ 在回复中向用户确认「确认提交吗?」
|
|
130
|
+
→ submitForm() // dangerous,用户确认后执行
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 翻页采集
|
|
134
|
+
|
|
135
|
+
`snapshotDOM` 属于 legacy-dom 组,先 `browser_discoverTools({ enable: ["browser_snapshotDOM"] })` 启用。
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
snapshotDOM({ selector: "[data-pagination]" }) // 找翻页结构
|
|
139
|
+
extractText({ selector: ".item", multiple: true })
|
|
140
|
+
→ 累积到内存
|
|
141
|
+
→ click({ selector: ".next" })
|
|
142
|
+
waitFor({ selector: ".item:nth-child(N+1)" })
|
|
143
|
+
→ 重复
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 跨 tab 协作
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
listTabs()
|
|
150
|
+
openTab({ url: "https://example.com" }) // 自动 attach
|
|
151
|
+
→ 在新 tab 操作
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Safety / quota rails
|
|
155
|
+
|
|
156
|
+
- **Quota**:默认 60 请求/分钟。超过会被拒绝,到 60s 自动恢复。先 `get_quota` 看额度。
|
|
157
|
+
- **Dangerous gating**:用户已在 sidepanel 配置了授权模式(read / default / trust / yolo),你不需要预判,调就是了,会被拦下来或自动通过。
|
|
158
|
+
- **超时**:默认每步 30s。慢操作(大页面 snapshot、长等待)记得显式传 `timeoutMs`。
|
|
159
|
+
|
|
160
|
+
## Don'ts
|
|
161
|
+
|
|
162
|
+
- ❌ 不要在 `runJS` 里直接读 cookie / localStorage(会被静态扫描归 dangerous,每次问审批)
|
|
163
|
+
- ❌ 不要循环调 `snapshotDOM`——单次抓全树用 `maxDepth` 而不是多次
|
|
164
|
+
- ❌ 不要在回复里频繁向用户确认(每次都要用户手动响应)
|
|
165
|
+
- ❌ 不要无视 `attachedTabs`——跨 tab 操作前确认目标 tab 未被其他会话占用(`list_tabs` 的 `busy`)
|
|
166
|
+
|
|
167
|
+
## When to use
|
|
168
|
+
|
|
169
|
+
- 用户要你「在网页上做某事」、「抓某站点数据」、「填某表单」
|
|
170
|
+
- 用户给了一个 URL 让你看
|
|
171
|
+
- 用户问「这页面在讲什么」
|
|
172
|
+
|
|
173
|
+
## When NOT to use
|
|
174
|
+
|
|
175
|
+
- 纯文本任务(写代码、改文件)不需要浏览器
|
|
176
|
+
- 用户明确说「不要打开浏览器」
|
|
177
|
+
- 网页需要复杂登录但用户没在浏览器里登录过
|
|
178
|
+
|
|
179
|
+
## Reference
|
|
180
|
+
|
|
181
|
+
- README: https://github.com/attson/atwebpilot
|
|
182
|
+
- 工具完整 schema 由 mcp-server 的 `tools/list` 暴露
|
|
183
|
+
|
|
184
|
+
## Observing a page: console, network, dialogs
|
|
185
|
+
|
|
186
|
+
These read from a recorder that is running on the page already, so you can ask
|
|
187
|
+
about things that happened *before* you called.
|
|
188
|
+
|
|
189
|
+
**Two backends, and you must check which one you got.** Every result carries
|
|
190
|
+
`backend`:
|
|
191
|
+
|
|
192
|
+
- `main-world` (default) — patches installed in the page's own realm. Cannot
|
|
193
|
+
see messages emitted before the patches installed, browser-level CORS/CSP
|
|
194
|
+
errors, or requests made by a service worker.
|
|
195
|
+
- `cdp` — `chrome.debugger`, opt-in from the extension settings. Sees all of
|
|
196
|
+
the above plus real response bodies.
|
|
197
|
+
|
|
198
|
+
If a result carries `degradedReason`, a CDP session was expected but is gone
|
|
199
|
+
(DevTools opened, or another extension took the tab). Treat the data as
|
|
200
|
+
incomplete rather than concluding the page did nothing. If it carries
|
|
201
|
+
`disabled`, the recorder is not on that page at all.
|
|
202
|
+
|
|
203
|
+
**Response bodies are off by default.** They cost memory on every page the user
|
|
204
|
+
visits, so arm them first and then reproduce:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
browser_recorderConfig({ bodies: true }) → arm
|
|
208
|
+
… trigger the request …
|
|
209
|
+
browser_networkRequests({ urlPattern: "/api/" })
|
|
210
|
+
browser_networkRequestDetail({ id: <id from above> })
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Calling `networkRequestDetail` without arming returns metadata plus
|
|
214
|
+
`bodyUnavailable` explaining the fix. Note this tool is **dangerous**: response
|
|
215
|
+
headers routinely carry `Authorization`, `Set-Cookie`, and bearer tokens.
|
|
216
|
+
|
|
217
|
+
**`handleDialog` is a pre-set policy on the default backend, not a reaction.**
|
|
218
|
+
`alert`, `confirm`, and `prompt` are synchronous, so a patched implementation
|
|
219
|
+
cannot pause and ask you. Declare what should happen *before* triggering the
|
|
220
|
+
dialog:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
browser_handleDialog({ accept: true, promptText: "yes", scope: "all" })
|
|
224
|
+
… click the thing that opens the dialog …
|
|
225
|
+
browser_handleDialog({ accept: true }) → also returns the recorded dialogs
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Under the CDP backend the dialog genuinely suspends, and a `handleDialog` call
|
|
229
|
+
answers the pending one immediately.
|
|
230
|
+
|
|
231
|
+
Until you arm it, dialogs behave exactly as they would without the extension —
|
|
232
|
+
the recorder never silently changes page behaviour.
|
|
233
|
+
|
|
234
|
+
## Debugging flow
|
|
235
|
+
|
|
236
|
+
When a page misbehaves, this ordering costs the least context:
|
|
237
|
+
|
|
238
|
+
1. `browser_consoleMessages({ level: "error" })` — cheap, and already buffered.
|
|
239
|
+
2. `browser_networkRequests({ status: 500 })` or `{ urlPattern: "/api/" }` —
|
|
240
|
+
summaries only.
|
|
241
|
+
3. `browser_recorderConfig({ bodies: true })`, reproduce, then
|
|
242
|
+
`browser_networkRequestDetail({ id })` for the one request that matters.
|
|
243
|
+
|
|
244
|
+
Poll incrementally with `sinceId` rather than re-reading the whole buffer. If a
|
|
245
|
+
result reports `dropped > 0`, the ring overflowed and you are seeing a window,
|
|
246
|
+
not the whole history.
|
|
247
|
+
|
|
248
|
+
## Multiple sessions share one browser
|
|
249
|
+
|
|
250
|
+
Several Claude Code sessions can be attached to the same browser at once, each
|
|
251
|
+
through its own connection. Two consequences worth knowing:
|
|
252
|
+
|
|
253
|
+
- **Pairing is non-blocking.** `list_tabs` lazily binds the port and opens a
|
|
254
|
+
confirmation page, then immediately returns `pairing_required` with
|
|
255
|
+
`pair_url`. Ask the user to click Allow. After they confirm, call
|
|
256
|
+
`pairing_status`; call `list_tabs` again only when it reports `connected`.
|
|
257
|
+
Never inspect the pair-page HTML or retry `list_tabs` while state is unchanged.
|
|
258
|
+
Once approved, later sessions on that machine connect silently.
|
|
259
|
+
- **Pick a free tab.** `list_tabs` marks tabs held by other sessions as `busy`.
|
|
260
|
+
Prefer an unowned tab, or open your own — two agents typing into one form
|
|
261
|
+
produces failures that look like your own tool calls not working.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@attson/atwebpilot-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.75",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "MCP server for the atwebpilot browser extension - let Codex or Claude Code drive your browser.",
|
|
6
6
|
"homepage": "https://github.com/attson/atwebpilot#readme",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
|
28
28
|
"test": "vitest run",
|
|
29
|
-
"build": "tsup",
|
|
29
|
+
"build": "tsup && node scripts/copy-skill.mjs",
|
|
30
30
|
"start": "tsx src/index.ts",
|
|
31
31
|
"analyze:sessions": "tsx src/session-analysis/cli.ts"
|
|
32
32
|
},
|