@attson/atwebpilot-mcp 0.0.80 → 0.0.82
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 +10 -5
- package/dist/index.js +516 -23
- package/dist/skill/SKILL.md +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
让 Codex 或 Claude Code 经一个本地 ws 中继驱动 atwebpilot 浏览器扩展操作网页(读 / 写 / 采)。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 给用户:安装
|
|
6
6
|
|
|
7
7
|
Codex:
|
|
8
8
|
|
|
9
|
+
npx -y @attson/atwebpilot-mcp --install-codex-skill
|
|
9
10
|
codex mcp add atwebpilot -- npx -y @attson/atwebpilot-mcp
|
|
10
11
|
|
|
12
|
+
第一条命令把正式 Skill 安装到用户级 `~/.agents/skills/atwebpilot-browser/`,让 Codex 在真实网页验收、截图、调试、读取或交互任务中主动发现 AtWebPilot;它不会修改任何项目的 `AGENTS.md`。内容相同时重复执行是幂等的;若目标被手工修改会拒绝覆盖,确认替换时显式加 `--force`。安装后新开 Codex 会话。
|
|
13
|
+
|
|
11
14
|
Claude Code:
|
|
12
15
|
|
|
13
16
|
claude mcp add atwebpilot --scope user -- npx -y @attson/atwebpilot-mcp
|
|
@@ -17,8 +20,8 @@ Claude Code:
|
|
|
17
20
|
**不需要手填端口。** 从 Plan 33 起是配对流程:
|
|
18
21
|
|
|
19
22
|
1. 会话启动时**不绑任何端口** —— 不碰网页的会话零副作用。
|
|
20
|
-
2. AI 第一次调 `
|
|
21
|
-
3.
|
|
23
|
+
2. AI 第一次调 `browser_start` 时才绑一个空闲端口,通过 MCP URL elicitation 请宿主打开配对页;不支持该能力的宿主回退到系统默认浏览器。`browser_start` 最多等待 45 秒。
|
|
24
|
+
3. 你在浏览器里点「允许」,扩展即接入;唯一 tab 可确定时同一次 `browser_start` 调用直接返回 ready session,多个候选时返回 `select_tab`,AI 带选中的 `tab_id` 再调用一次。无需让你回复「已连接」。只有等待超时才返回 `pairing_required + pair_url`;`pairing_status` 始终是立即返回的非阻塞状态检查。
|
|
22
25
|
4. 之后本机的会话都免确认(信任是安装级的);端口能复用时连配对页都不会弹。
|
|
23
26
|
|
|
24
27
|
若宿主和系统都未弹出浏览器,可在超时结果中打开 `pair_url`。授权成功只以扩展实际建立连接并发送 `HELLO` 为准;打开配对页本身不代表已连接。
|
|
@@ -30,6 +33,7 @@ Coordinator 查看已接入的会话、单独断开,或撤销信任。
|
|
|
30
33
|
|
|
31
34
|
- `ATWEBPILOT_WS_PORT`:固定端口(默认是自动选一个空闲端口并复用上次的)
|
|
32
35
|
- `ATWEBPILOT_WS_TOKEN`:要求扩展带 `bearer.<token>` 子协议
|
|
36
|
+
- `ATWEBPILOT_CODEX_SKILLS_DIR`:覆盖 Codex Skill 安装根目录(默认 `~/.agents/skills`)
|
|
33
37
|
|
|
34
38
|
## 给开发者:本地 monorepo
|
|
35
39
|
|
|
@@ -48,9 +52,10 @@ listen 端口的能力,所以方向只能如此。这也是配对页存在的
|
|
|
48
52
|
|
|
49
53
|
## 工具面
|
|
50
54
|
|
|
51
|
-
- 控制面
|
|
55
|
+
- 控制面 6 个:`browser_start / pairing_status / list_tabs / open_session / close_session / get_quota`。常规网页任务从 `browser_start` 开始;`list_tabs` / `open_session` 保留为底层兼容入口。
|
|
52
56
|
- 发现 1 个:`browser_discoverTools` —— 已知工具时直接传 `enable: [...]`,已知分组时直接传 `enableGroups: ["export", ...]`,也可用 `profile: "core" | "full"` 切换整套工具面;只有这些都未知时才不带参数读取目录。选中的工具会加入本进程的 `tools/list`(发送 `tools/list_changed`),并始终返回完整 schema,包括已经启用的工具。
|
|
53
57
|
- 同轮执行桥 1 个:`browser_call({session_id, tool, args})` —— 某些宿主虽已刷新 `tools/list`,却不会在当前模型轮次注入新工具。AI 可在 `discoverTools` 后立刻通过该桥执行,不需要让用户回复“继续”;下一轮看到原生工具后仍优先使用原生工具。
|
|
58
|
+
- 批量编排 1 个:`browser_runSteps({session_id, steps, return_bindings?})` —— 在步骤和参数已经明确时,把 1–20 个 safe/caution 工具合并为一次 MCP 调用;`${binding}` 可引用前序结果。整批先做工具支持、权限和配额预检,dangerous、脚本、截图、下载与 tab 生命周期操作保持单步。
|
|
54
59
|
- 执行面在浏览器连接前不进入 `tools/list`;连接确认后加载默认 **core 31 个** `browser_*`。其余 20 个由 AI 按需 `discoverTools` 拉取,用户不用配置。
|
|
55
60
|
- 不暴露 `askUser`(MCP 会话没有人在侧边栏应答)和 `attachTab` / `detachTab`(目标 tab 已由 `open_session` 绑定)。
|
|
56
61
|
|
|
@@ -66,7 +71,7 @@ hover / uploadFile / scroll / waitFor`);`browser_storage` 也不会列出,
|
|
|
66
71
|
|
|
67
72
|
| 值 | 效果 |
|
|
68
73
|
|---|---|
|
|
69
|
-
| `core`(默认) |
|
|
74
|
+
| `core`(默认) | 连接前仅控制面、发现工具、同轮执行桥与批量编排;连接后再列出 31 个核心工具 |
|
|
70
75
|
| `full` | 连接前同上;连接后列出全部 51 个原生工具 |
|
|
71
76
|
|
|
72
77
|
无法识别的值(包括已移除的 `parity`)按 `core` 处理,并往 stderr 打一条提示。
|
package/dist/index.js
CHANGED
|
@@ -946,6 +946,20 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprot
|
|
|
946
946
|
|
|
947
947
|
// src/control-tools.ts
|
|
948
948
|
var CONTROL_TOOLS = [
|
|
949
|
+
{
|
|
950
|
+
name: "browser_start",
|
|
951
|
+
description: "Primary entry point for a live web page in the user's existing Chrome: localhost, screenshots/UI smoke, authenticated interaction, or console/network debugging. Pairs if needed and returns a ready session or tab candidates.",
|
|
952
|
+
inputSchema: {
|
|
953
|
+
type: "object",
|
|
954
|
+
properties: {
|
|
955
|
+
tab_id: { type: "string", description: "Exact id from browser_start/list_tabs" },
|
|
956
|
+
url: { type: "string", description: "Exact URL of an open tab" },
|
|
957
|
+
capabilities: { type: "array", items: { type: "string" }, description: "Optional capability scope" },
|
|
958
|
+
idle_timeout_min: { type: "number", description: "Optional idle timeout in minutes" }
|
|
959
|
+
},
|
|
960
|
+
additionalProperties: false
|
|
961
|
+
}
|
|
962
|
+
},
|
|
949
963
|
{
|
|
950
964
|
name: "pairing_status",
|
|
951
965
|
description: "Start or check browser pairing without waiting. Returns connected or pairing_required with the pair URL.",
|
|
@@ -2389,6 +2403,53 @@ async function handleListTabs(deps, onPairingRequired, timeoutMs = LIST_TABS_PAI
|
|
|
2389
2403
|
}
|
|
2390
2404
|
return { status: "connected", tabs: worker.available_tabs };
|
|
2391
2405
|
}
|
|
2406
|
+
function selectionResult(tabs, message) {
|
|
2407
|
+
return { status: "select_tab", tabs, message };
|
|
2408
|
+
}
|
|
2409
|
+
async function handleBrowserStart(deps, args, onPairingRequired, timeoutMs = LIST_TABS_PAIRING_TIMEOUT_MS) {
|
|
2410
|
+
const listed = await handleListTabs(deps, onPairingRequired, timeoutMs);
|
|
2411
|
+
if (listed.status === "pairing_required") return listed;
|
|
2412
|
+
const tabId = typeof args.tab_id === "string" && args.tab_id.length > 0 ? args.tab_id : void 0;
|
|
2413
|
+
const url = typeof args.url === "string" && args.url.length > 0 ? args.url : void 0;
|
|
2414
|
+
let selected;
|
|
2415
|
+
if (tabId) {
|
|
2416
|
+
selected = listed.tabs.find((tab) => tab.tab_id === tabId);
|
|
2417
|
+
if (!selected) {
|
|
2418
|
+
throw new Error(`InvalidArgs: browser_start tab_id ${JSON.stringify(tabId)} is not available`);
|
|
2419
|
+
}
|
|
2420
|
+
if (url && selected.url !== url) {
|
|
2421
|
+
throw new Error("InvalidArgs: browser_start tab_id and url must identify the same tab");
|
|
2422
|
+
}
|
|
2423
|
+
} else if (url) {
|
|
2424
|
+
const matches = listed.tabs.filter((tab) => tab.url === url);
|
|
2425
|
+
if (matches.length === 1) {
|
|
2426
|
+
[selected] = matches;
|
|
2427
|
+
} else if (matches.length > 1) {
|
|
2428
|
+
return selectionResult(
|
|
2429
|
+
matches,
|
|
2430
|
+
"Multiple tabs have that exact URL. Choose one and call browser_start again with tab_id."
|
|
2431
|
+
);
|
|
2432
|
+
} else {
|
|
2433
|
+
return selectionResult(
|
|
2434
|
+
listed.tabs,
|
|
2435
|
+
"No exact URL match. Choose a tab and call browser_start again with tab_id."
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
} else if (listed.tabs.length === 1) {
|
|
2439
|
+
[selected] = listed.tabs;
|
|
2440
|
+
} else {
|
|
2441
|
+
return selectionResult(
|
|
2442
|
+
listed.tabs,
|
|
2443
|
+
"Choose a tab and call browser_start again with tab_id."
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
const opened = await handleOpenSession(deps, {
|
|
2447
|
+
tab_id: selected.tab_id,
|
|
2448
|
+
...Array.isArray(args.capabilities) ? { capabilities: args.capabilities } : {},
|
|
2449
|
+
...typeof args.idle_timeout_min === "number" ? { idle_timeout_min: args.idle_timeout_min } : {}
|
|
2450
|
+
});
|
|
2451
|
+
return { status: "ready", session_id: opened.session_id, tab: selected };
|
|
2452
|
+
}
|
|
2392
2453
|
async function handleOpenSession(deps, args) {
|
|
2393
2454
|
const { coordinator } = await deps.ensure();
|
|
2394
2455
|
const worker_id = singleWorkerId(coordinator, deps.pairUrl());
|
|
@@ -2414,7 +2475,7 @@ var NON_BUILTIN_CAPABILITY = {
|
|
|
2414
2475
|
listTabs: "tab:open",
|
|
2415
2476
|
openTab: "tab:open"
|
|
2416
2477
|
};
|
|
2417
|
-
async function
|
|
2478
|
+
async function prepareBrowserTool(deps, gen, args) {
|
|
2418
2479
|
const { coordinator, hub } = await deps.ensure();
|
|
2419
2480
|
const session_id = String(args.session_id);
|
|
2420
2481
|
const session = coordinator.sessions.get(session_id);
|
|
@@ -2429,23 +2490,20 @@ async function handleBrowserTool(deps, gen, args) {
|
|
|
2429
2490
|
const unsafe = highestSeverity(runStaticScan(source)) === "dangerous";
|
|
2430
2491
|
const v = coordinator.validateCall({ session_id, kind: "runJS", unsafe });
|
|
2431
2492
|
if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
|
|
2432
|
-
coordinator.recordCall(session_id, v.dangerous);
|
|
2433
2493
|
const requestedTimeout = toolArgs.timeoutMs;
|
|
2434
|
-
|
|
2494
|
+
return {
|
|
2495
|
+
coordinator,
|
|
2496
|
+
hub,
|
|
2435
2497
|
session_id,
|
|
2498
|
+
worker_id: session.worker_id,
|
|
2436
2499
|
tab_id: session.tab_id,
|
|
2437
2500
|
step: {
|
|
2438
2501
|
kind: "js",
|
|
2439
2502
|
source,
|
|
2440
2503
|
...typeof requestedTimeout === "number" && Number.isInteger(requestedTimeout) && requestedTimeout > 0 ? { timeoutMs: requestedTimeout } : {}
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
throw new Error(
|
|
2445
|
-
jsResult.error ? `${jsResult.error.code}: ${jsResult.error.message}` : "EXEC failed"
|
|
2446
|
-
);
|
|
2447
|
-
}
|
|
2448
|
-
return jsResult.return ?? null;
|
|
2504
|
+
},
|
|
2505
|
+
dangerous: v.dangerous
|
|
2506
|
+
};
|
|
2449
2507
|
}
|
|
2450
2508
|
if (tool === "switchToTab" || tool === "closeTab") {
|
|
2451
2509
|
const boundTabId = Number.parseInt(session.tab_id, 10);
|
|
@@ -2455,10 +2513,11 @@ async function handleBrowserTool(deps, gen, args) {
|
|
|
2455
2513
|
toolArgs.tabId = boundTabId;
|
|
2456
2514
|
}
|
|
2457
2515
|
const override = NON_BUILTIN_CAPABILITY[tool];
|
|
2516
|
+
let dangerous;
|
|
2458
2517
|
if (override) {
|
|
2459
2518
|
const v = coordinator.validateCall({ session_id, kind: "capability", capability: override });
|
|
2460
2519
|
if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
|
|
2461
|
-
|
|
2520
|
+
dangerous = v.dangerous;
|
|
2462
2521
|
} else {
|
|
2463
2522
|
const raw = toolArgs;
|
|
2464
2523
|
const v = coordinator.validateCall({
|
|
@@ -2470,13 +2529,321 @@ async function handleBrowserTool(deps, gen, args) {
|
|
|
2470
2529
|
recorderArmsBodies: tool === "recorderConfig" ? raw.bodies === true : void 0
|
|
2471
2530
|
});
|
|
2472
2531
|
if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
|
|
2473
|
-
|
|
2532
|
+
dangerous = v.dangerous;
|
|
2474
2533
|
}
|
|
2475
|
-
|
|
2534
|
+
return {
|
|
2535
|
+
coordinator,
|
|
2536
|
+
hub,
|
|
2537
|
+
session_id,
|
|
2538
|
+
worker_id: session.worker_id,
|
|
2539
|
+
tab_id: session.tab_id,
|
|
2540
|
+
step: { kind: "tool", tool, args: toolArgs },
|
|
2541
|
+
dangerous
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
async function handleBrowserTool(deps, gen, args) {
|
|
2545
|
+
const prepared = await prepareBrowserTool(deps, gen, args);
|
|
2546
|
+
prepared.coordinator.recordCall(prepared.session_id, prepared.dangerous);
|
|
2547
|
+
const result = await prepared.hub.exec(prepared.worker_id, {
|
|
2548
|
+
session_id: prepared.session_id,
|
|
2549
|
+
tab_id: prepared.tab_id,
|
|
2550
|
+
step: prepared.step
|
|
2551
|
+
});
|
|
2476
2552
|
if (!result.ok) throw new Error(result.error ? `${result.error.code}: ${result.error.message}` : "EXEC failed");
|
|
2477
2553
|
return result.return ?? null;
|
|
2478
2554
|
}
|
|
2479
2555
|
|
|
2556
|
+
// src/browser-run-steps.ts
|
|
2557
|
+
var BROWSER_RUN_STEPS_TOOL = {
|
|
2558
|
+
name: "browser_runSteps",
|
|
2559
|
+
description: "Run 1-20 enabled safe/caution browser tools sequentially in one MCP call. Supports ${binding} references and returns only the final result plus requested bindings.",
|
|
2560
|
+
inputSchema: {
|
|
2561
|
+
type: "object",
|
|
2562
|
+
required: ["session_id", "steps"],
|
|
2563
|
+
properties: {
|
|
2564
|
+
session_id: { type: "string", description: "Session id from open_session" },
|
|
2565
|
+
steps: {
|
|
2566
|
+
type: "array",
|
|
2567
|
+
minItems: 1,
|
|
2568
|
+
maxItems: 20,
|
|
2569
|
+
items: {
|
|
2570
|
+
type: "object",
|
|
2571
|
+
required: ["tool", "args"],
|
|
2572
|
+
properties: {
|
|
2573
|
+
tool: {
|
|
2574
|
+
type: "string",
|
|
2575
|
+
description: "Enabled browser tool name; browser_ prefix is optional"
|
|
2576
|
+
},
|
|
2577
|
+
args: {
|
|
2578
|
+
type: "object",
|
|
2579
|
+
description: "Tool arguments excluding session_id; may reference earlier ${bindings}",
|
|
2580
|
+
additionalProperties: true
|
|
2581
|
+
},
|
|
2582
|
+
bind_result_to: {
|
|
2583
|
+
type: "string",
|
|
2584
|
+
pattern: "^[A-Za-z_][A-Za-z0-9_]{0,63}$"
|
|
2585
|
+
}
|
|
2586
|
+
},
|
|
2587
|
+
additionalProperties: false
|
|
2588
|
+
}
|
|
2589
|
+
},
|
|
2590
|
+
return_bindings: {
|
|
2591
|
+
type: "array",
|
|
2592
|
+
items: {
|
|
2593
|
+
type: "string",
|
|
2594
|
+
pattern: "^[A-Za-z_][A-Za-z0-9_]{0,63}$"
|
|
2595
|
+
},
|
|
2596
|
+
uniqueItems: true
|
|
2597
|
+
}
|
|
2598
|
+
},
|
|
2599
|
+
additionalProperties: false
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
var BINDING_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
2603
|
+
var BLOCKED_BATCH_TOOLS = /* @__PURE__ */ new Set([
|
|
2604
|
+
"browser_downloadImage",
|
|
2605
|
+
"browser_downloadSpreadsheet",
|
|
2606
|
+
"browser_openTab",
|
|
2607
|
+
"browser_closeTab",
|
|
2608
|
+
"browser_switchToTab"
|
|
2609
|
+
]);
|
|
2610
|
+
var CAPABILITY_SENSITIVE_FIELDS = /* @__PURE__ */ new Map([
|
|
2611
|
+
["browser_httpRequest", ["withCredentials"]],
|
|
2612
|
+
["browser_drop", ["files"]],
|
|
2613
|
+
["browser_recorderConfig", ["bodies"]]
|
|
2614
|
+
]);
|
|
2615
|
+
function runStepsPreflightError(message) {
|
|
2616
|
+
return {
|
|
2617
|
+
ok: false,
|
|
2618
|
+
phase: "preflight",
|
|
2619
|
+
completed: 0,
|
|
2620
|
+
error: { message },
|
|
2621
|
+
bindings: {}
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
async function handleBrowserRunSteps(deps, args, context) {
|
|
2625
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id : "";
|
|
2626
|
+
if (!sessionId) throw new Error("InvalidArgs: browser_runSteps requires session_id");
|
|
2627
|
+
if (!Array.isArray(args.steps) || args.steps.length < 1 || args.steps.length > 20) {
|
|
2628
|
+
throw new Error("InvalidArgs: browser_runSteps requires 1-20 steps");
|
|
2629
|
+
}
|
|
2630
|
+
const steps = args.steps.map(parseStep);
|
|
2631
|
+
const requested = parseReturnBindings(args.return_bindings);
|
|
2632
|
+
const bindings = /* @__PURE__ */ Object.create(null);
|
|
2633
|
+
let result = null;
|
|
2634
|
+
const bindingIssue = validateBindingPlan(steps, requested);
|
|
2635
|
+
if (bindingIssue) {
|
|
2636
|
+
return preflightFailure(steps, bindingIssue.index, bindingIssue.message);
|
|
2637
|
+
}
|
|
2638
|
+
const { coordinator } = await deps.ensure();
|
|
2639
|
+
const quota = coordinator.quotaFor(sessionId);
|
|
2640
|
+
if (!quota) throw new Error(`session ${sessionId} not found`);
|
|
2641
|
+
const remainingSteps = quota.max_steps - quota.steps_used;
|
|
2642
|
+
if (steps.length > remainingSteps) {
|
|
2643
|
+
const failedIndex = Math.max(0, remainingSteps);
|
|
2644
|
+
return preflightFailure(
|
|
2645
|
+
steps,
|
|
2646
|
+
failedIndex,
|
|
2647
|
+
`Session has only ${remainingSteps} steps remaining; batch requires ${steps.length}`
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
for (let index = 0; index < steps.length; index++) {
|
|
2651
|
+
const step = steps[index];
|
|
2652
|
+
const name = browserToolName(step.tool);
|
|
2653
|
+
const generated = context.tools.get(name);
|
|
2654
|
+
try {
|
|
2655
|
+
assertAvailable(name, step.tool, generated, context);
|
|
2656
|
+
if (generated.stepKind === "js" || generated.resultKind === "image" || BLOCKED_BATCH_TOOLS.has(name)) {
|
|
2657
|
+
throw new Error(`Tool ${name} is not allowed in browser_runSteps`);
|
|
2658
|
+
}
|
|
2659
|
+
for (const field of CAPABILITY_SENSITIVE_FIELDS.get(name) ?? []) {
|
|
2660
|
+
if (bindingReferences(step.args[field]).length > 0) {
|
|
2661
|
+
throw new Error(
|
|
2662
|
+
`Capability-sensitive field ${field} cannot reference bindings in browser_runSteps`
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
const prepared = await prepareBrowserTool(deps, generated, {
|
|
2667
|
+
...step.args,
|
|
2668
|
+
session_id: sessionId
|
|
2669
|
+
});
|
|
2670
|
+
if (prepared.dangerous) {
|
|
2671
|
+
throw new Error("Dangerous tool is not allowed in browser_runSteps");
|
|
2672
|
+
}
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
return {
|
|
2675
|
+
ok: false,
|
|
2676
|
+
phase: "preflight",
|
|
2677
|
+
completed: 0,
|
|
2678
|
+
failed_step: { index, tool: name },
|
|
2679
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
2680
|
+
bindings: {}
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
for (let index = 0; index < steps.length; index++) {
|
|
2685
|
+
const step = steps[index];
|
|
2686
|
+
const name = browserToolName(step.tool);
|
|
2687
|
+
const generated = context.tools.get(name);
|
|
2688
|
+
assertAvailable(name, step.tool, generated, context);
|
|
2689
|
+
const resolvedArgs = resolveBindings(step.args, bindings);
|
|
2690
|
+
try {
|
|
2691
|
+
result = await handleBrowserTool(deps, generated, { ...resolvedArgs, session_id: sessionId });
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
return {
|
|
2694
|
+
ok: false,
|
|
2695
|
+
phase: "execution",
|
|
2696
|
+
completed: index,
|
|
2697
|
+
failed_step: { index, tool: name },
|
|
2698
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
2699
|
+
bindings: selectBindings(requested, bindings, false)
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
if (step.bind_result_to) bindings[step.bind_result_to] = result;
|
|
2703
|
+
}
|
|
2704
|
+
return {
|
|
2705
|
+
ok: true,
|
|
2706
|
+
completed: steps.length,
|
|
2707
|
+
result,
|
|
2708
|
+
bindings: selectBindings(requested, bindings, true)
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
function preflightFailure(steps, index, message) {
|
|
2712
|
+
const boundedIndex = Math.min(Math.max(index, 0), steps.length - 1);
|
|
2713
|
+
return {
|
|
2714
|
+
ok: false,
|
|
2715
|
+
phase: "preflight",
|
|
2716
|
+
completed: 0,
|
|
2717
|
+
failed_step: {
|
|
2718
|
+
index: boundedIndex,
|
|
2719
|
+
tool: browserToolName(steps[boundedIndex].tool)
|
|
2720
|
+
},
|
|
2721
|
+
error: { message },
|
|
2722
|
+
bindings: {}
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
function parseReturnBindings(value) {
|
|
2726
|
+
if (value === void 0) return [];
|
|
2727
|
+
if (!Array.isArray(value) || value.some((name) => typeof name !== "string")) {
|
|
2728
|
+
throw new Error("InvalidArgs: return_bindings must be an array of binding names");
|
|
2729
|
+
}
|
|
2730
|
+
return value;
|
|
2731
|
+
}
|
|
2732
|
+
function validateBindingPlan(steps, requested) {
|
|
2733
|
+
const declared = /* @__PURE__ */ new Set();
|
|
2734
|
+
for (let index = 0; index < steps.length; index++) {
|
|
2735
|
+
const step = steps[index];
|
|
2736
|
+
for (const name of bindingReferences(step.args)) {
|
|
2737
|
+
if (!declared.has(name)) {
|
|
2738
|
+
return {
|
|
2739
|
+
index,
|
|
2740
|
+
message: `InvalidArgs: binding ${name} must be declared by an earlier step`
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
if (step.bind_result_to) {
|
|
2745
|
+
if (!BINDING_NAME.test(step.bind_result_to)) {
|
|
2746
|
+
return { index, message: `InvalidArgs: invalid binding name ${step.bind_result_to}` };
|
|
2747
|
+
}
|
|
2748
|
+
if (declared.has(step.bind_result_to)) {
|
|
2749
|
+
return { index, message: `InvalidArgs: duplicate binding ${step.bind_result_to}` };
|
|
2750
|
+
}
|
|
2751
|
+
declared.add(step.bind_result_to);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
const seenRequested = /* @__PURE__ */ new Set();
|
|
2755
|
+
for (const name of requested) {
|
|
2756
|
+
if (!BINDING_NAME.test(name)) {
|
|
2757
|
+
return { index: 0, message: `InvalidArgs: invalid return binding ${name}` };
|
|
2758
|
+
}
|
|
2759
|
+
if (seenRequested.has(name)) {
|
|
2760
|
+
return { index: 0, message: `InvalidArgs: duplicate return binding ${name}` };
|
|
2761
|
+
}
|
|
2762
|
+
if (!declared.has(name)) {
|
|
2763
|
+
return { index: 0, message: `InvalidArgs: unknown return binding ${name}` };
|
|
2764
|
+
}
|
|
2765
|
+
seenRequested.add(name);
|
|
2766
|
+
}
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2769
|
+
function bindingReferences(value) {
|
|
2770
|
+
if (typeof value === "string") {
|
|
2771
|
+
return [...value.matchAll(/\$\{([^}]+)\}/g)].map((match) => match[1]);
|
|
2772
|
+
}
|
|
2773
|
+
if (Array.isArray(value)) return value.flatMap(bindingReferences);
|
|
2774
|
+
if (value && typeof value === "object") {
|
|
2775
|
+
return Object.values(value).flatMap(bindingReferences);
|
|
2776
|
+
}
|
|
2777
|
+
return [];
|
|
2778
|
+
}
|
|
2779
|
+
function assertAvailable(name, rawName, generated, context) {
|
|
2780
|
+
if (!generated) throw new Error(`Unknown browser tool: ${rawName}`);
|
|
2781
|
+
if (!context.advertised.has(name)) {
|
|
2782
|
+
throw new Error(`${name} is not enabled; call browser_discoverTools first`);
|
|
2783
|
+
}
|
|
2784
|
+
if (context.supported && !generated.builtinTools.every((tool) => context.supported.has(tool))) {
|
|
2785
|
+
throw new Error(`${name} is not supported by the connected browser extension`);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
function parseStep(value) {
|
|
2789
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2790
|
+
throw new Error("InvalidArgs: each browser_runSteps step must be an object");
|
|
2791
|
+
}
|
|
2792
|
+
const raw = value;
|
|
2793
|
+
if (typeof raw.tool !== "string" || raw.tool.trim() === "") {
|
|
2794
|
+
throw new Error("InvalidArgs: each browser_runSteps step requires tool");
|
|
2795
|
+
}
|
|
2796
|
+
if (!raw.args || typeof raw.args !== "object" || Array.isArray(raw.args)) {
|
|
2797
|
+
throw new Error("InvalidArgs: each browser_runSteps step requires object args");
|
|
2798
|
+
}
|
|
2799
|
+
if (raw.bind_result_to !== void 0 && typeof raw.bind_result_to !== "string") {
|
|
2800
|
+
throw new Error("InvalidArgs: bind_result_to must be a binding name");
|
|
2801
|
+
}
|
|
2802
|
+
return {
|
|
2803
|
+
tool: raw.tool,
|
|
2804
|
+
args: raw.args,
|
|
2805
|
+
...typeof raw.bind_result_to === "string" ? { bind_result_to: raw.bind_result_to } : {}
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
function browserToolName(raw) {
|
|
2809
|
+
const name = raw.trim();
|
|
2810
|
+
return name.startsWith("browser_") ? name : `browser_${name}`;
|
|
2811
|
+
}
|
|
2812
|
+
function resolveBindings(value, bindings) {
|
|
2813
|
+
if (typeof value === "string") {
|
|
2814
|
+
const exact = value.match(/^\$\{([^}]+)\}$/);
|
|
2815
|
+
if (exact) return requireBinding(exact[1], bindings);
|
|
2816
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
|
|
2817
|
+
const bound = requireBinding(name, bindings);
|
|
2818
|
+
return typeof bound === "string" ? bound : JSON.stringify(bound);
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
if (Array.isArray(value)) return value.map((item) => resolveBindings(item, bindings));
|
|
2822
|
+
if (value && typeof value === "object") {
|
|
2823
|
+
const resolved = {};
|
|
2824
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2825
|
+
resolved[key] = resolveBindings(item, bindings);
|
|
2826
|
+
}
|
|
2827
|
+
return resolved;
|
|
2828
|
+
}
|
|
2829
|
+
return value;
|
|
2830
|
+
}
|
|
2831
|
+
function requireBinding(name, bindings) {
|
|
2832
|
+
if (!(name in bindings)) throw new Error(`InvalidArgs: binding ${name} is not available`);
|
|
2833
|
+
return bindings[name];
|
|
2834
|
+
}
|
|
2835
|
+
function selectBindings(requested, bindings, requireAll) {
|
|
2836
|
+
const selected = {};
|
|
2837
|
+
for (const name of requested) {
|
|
2838
|
+
if (!(name in bindings)) {
|
|
2839
|
+
if (requireAll) throw new Error(`InvalidArgs: unknown return binding ${name}`);
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
selected[name] = bindings[name];
|
|
2843
|
+
}
|
|
2844
|
+
return selected;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2480
2847
|
// src/skill-bundle.ts
|
|
2481
2848
|
import { readFileSync } from "fs";
|
|
2482
2849
|
import { dirname, join } from "path";
|
|
@@ -2486,12 +2853,13 @@ var SKILL_DESCRIPTION = "Strategy + scenarios + safety rails for driving the AtW
|
|
|
2486
2853
|
var TOPICS = {
|
|
2487
2854
|
quickstart: `# AtWebPilot quickstart
|
|
2488
2855
|
|
|
2489
|
-
1. Call
|
|
2490
|
-
2. If
|
|
2491
|
-
3.
|
|
2856
|
+
1. Call browser_start once. On first use it opens the pairing page and waits for authorization; when one tab is unambiguous it returns a ready session in the same call.
|
|
2857
|
+
2. If browser_start returns select_tab, call browser_start again with one returned tab_id. list_tabs and open_session remain lower-level compatibility tools.
|
|
2858
|
+
3. If browser_start times out and returns pairing_required, show pair_url and wait for browser approval. Use pairing_status for a later non-blocking status check; do not ask the user to reply just to continue.
|
|
2492
2859
|
4. For interaction use browser_getPageInfo then browser_takeSnapshot; for broad reading use browser_createPageIndex.
|
|
2493
|
-
5.
|
|
2494
|
-
6.
|
|
2860
|
+
5. When 2-20 safe/caution steps and their arguments are already known, use browser_runSteps to execute them in one MCP call. Keep dangerous actions and steps needing intermediate inspection as separate calls.
|
|
2861
|
+
6. Prefer purpose-built tools and verified results. Use browser_discoverTools only for capabilities outside the connected core surface. Its response always includes the selected schemas. If a newly enabled native tool is not visible in the current turn, call it immediately with browser_call; do not ask the user to send "continue".
|
|
2862
|
+
7. Close the session when finished.
|
|
2495
2863
|
`,
|
|
2496
2864
|
pairing: `# AtWebPilot pairing
|
|
2497
2865
|
|
|
@@ -2537,9 +2905,11 @@ function readSkillBundle(rawTopic = "quickstart") {
|
|
|
2537
2905
|
}
|
|
2538
2906
|
if (cached == null) {
|
|
2539
2907
|
try {
|
|
2540
|
-
cached =
|
|
2908
|
+
cached = `${TOPICS.quickstart}
|
|
2909
|
+
${readFileSync(locateSkillFile(), "utf-8")}`;
|
|
2541
2910
|
} catch {
|
|
2542
|
-
cached =
|
|
2911
|
+
cached = `${TOPICS.quickstart}
|
|
2912
|
+
# ${SKILL_NAME}
|
|
2543
2913
|
|
|
2544
2914
|
${SKILL_DESCRIPTION}
|
|
2545
2915
|
|
|
@@ -2569,7 +2939,7 @@ var SKILL_TOOL = {
|
|
|
2569
2939
|
// package.json
|
|
2570
2940
|
var package_default = {
|
|
2571
2941
|
name: "@attson/atwebpilot-mcp",
|
|
2572
|
-
version: "0.0.
|
|
2942
|
+
version: "0.0.82",
|
|
2573
2943
|
license: "Apache-2.0",
|
|
2574
2944
|
description: "MCP server for the atwebpilot browser extension - let Codex or Claude Code drive your browser.",
|
|
2575
2945
|
homepage: "https://github.com/attson/atwebpilot#readme",
|
|
@@ -2678,6 +3048,7 @@ function createPairingPresenter(server, fallbackOpenUrl) {
|
|
|
2678
3048
|
}
|
|
2679
3049
|
|
|
2680
3050
|
// src/mcp-server.ts
|
|
3051
|
+
var MCP_INSTRUCTIONS = "Use AtWebPilot for tasks that require inspecting, testing, debugging, screenshotting, reading, or interacting with a live web page in the user's existing Chrome, including localhost and authenticated pages. Start with browser_start. When AtWebPilot is available, use it before probing or launching Playwright, Puppeteer, or Chrome remote debugging. Do not start the browser for pure code editing or static source analysis that does not require observing a live page.";
|
|
2681
3052
|
var ALL_BROWSER_TOOLS = generateBrowserTools("full");
|
|
2682
3053
|
var BROWSER_BY_NAME = new Map(ALL_BROWSER_TOOLS.map((t) => [t.name, t]));
|
|
2683
3054
|
function createToolState(mode) {
|
|
@@ -2724,6 +3095,7 @@ function buildToolList(deps, state = DEFAULT_STATE) {
|
|
|
2724
3095
|
...CONTROL_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
2725
3096
|
{ name: DISCOVER_TOOL.name, description: DISCOVER_TOOL.description, inputSchema: DISCOVER_TOOL.inputSchema },
|
|
2726
3097
|
{ name: BROWSER_CALL_TOOL.name, description: BROWSER_CALL_TOOL.description, inputSchema: BROWSER_CALL_TOOL.inputSchema },
|
|
3098
|
+
{ name: BROWSER_RUN_STEPS_TOOL.name, description: BROWSER_RUN_STEPS_TOOL.description, inputSchema: BROWSER_RUN_STEPS_TOOL.inputSchema },
|
|
2727
3099
|
...browser.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
|
|
2728
3100
|
];
|
|
2729
3101
|
}
|
|
@@ -2763,6 +3135,24 @@ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChang
|
|
|
2763
3135
|
if (name === BROWSER_CALL_TOOL.name) {
|
|
2764
3136
|
return handleBrowserCall(deps, args, state);
|
|
2765
3137
|
}
|
|
3138
|
+
if (name === BROWSER_RUN_STEPS_TOOL.name) {
|
|
3139
|
+
let result;
|
|
3140
|
+
try {
|
|
3141
|
+
result = await handleBrowserRunSteps(deps, args, {
|
|
3142
|
+
tools: BROWSER_BY_NAME,
|
|
3143
|
+
advertised: state.advertised,
|
|
3144
|
+
supported: workerToolSupport(deps)
|
|
3145
|
+
});
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3148
|
+
if (message.includes("PAIRING_REQUIRED")) {
|
|
3149
|
+
const pairUrl = deps.pairUrl();
|
|
3150
|
+
if (pairUrl) onPairingRequired?.(pairUrl);
|
|
3151
|
+
}
|
|
3152
|
+
result = runStepsPreflightError(message);
|
|
3153
|
+
}
|
|
3154
|
+
return result.ok ? ok(result) : { ...ok(result), isError: true };
|
|
3155
|
+
}
|
|
2766
3156
|
if (name === "pairing_status") {
|
|
2767
3157
|
const result = await handlePairingStatus(deps);
|
|
2768
3158
|
if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
|
|
@@ -2774,6 +3164,11 @@ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChang
|
|
|
2774
3164
|
if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
|
|
2775
3165
|
return ok(result);
|
|
2776
3166
|
}
|
|
3167
|
+
if (name === "browser_start") {
|
|
3168
|
+
const result = await handleBrowserStart(deps, args, onPairingRequired);
|
|
3169
|
+
if (result.status !== "pairing_required") await revealBrowserSurface(state, onListChanged);
|
|
3170
|
+
return ok(result);
|
|
3171
|
+
}
|
|
2777
3172
|
if (name === "open_session") {
|
|
2778
3173
|
const result = await handleOpenSession(deps, args);
|
|
2779
3174
|
await revealBrowserSurface(state, onListChanged);
|
|
@@ -2826,7 +3221,10 @@ async function revealBrowserSurface(state, onListChanged) {
|
|
|
2826
3221
|
function createMcpServer(deps, state = DEFAULT_STATE, options = {}) {
|
|
2827
3222
|
const server = new Server(
|
|
2828
3223
|
{ name: "atwebpilot-mcp", version: MCP_VERSION },
|
|
2829
|
-
{
|
|
3224
|
+
{
|
|
3225
|
+
capabilities: { tools: { listChanged: true }, logging: {} },
|
|
3226
|
+
instructions: MCP_INSTRUCTIONS
|
|
3227
|
+
}
|
|
2830
3228
|
);
|
|
2831
3229
|
const presentPairing = createPairingPresenter(server, options.fallbackOpenUrl);
|
|
2832
3230
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList(deps, state) }));
|
|
@@ -3603,8 +4001,103 @@ async function defaultOpenUrl(url) {
|
|
|
3603
4001
|
throw new Error(errors.join("; ") || "no browser opener available");
|
|
3604
4002
|
}
|
|
3605
4003
|
|
|
4004
|
+
// src/skill-installer.ts
|
|
4005
|
+
import { access, mkdir, readFile as readFile2, rename, unlink, writeFile } from "fs/promises";
|
|
4006
|
+
import { homedir as homedir2 } from "os";
|
|
4007
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
4008
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4009
|
+
var SKILL_DIRECTORY_NAME = "atwebpilot-browser";
|
|
4010
|
+
function isMissing(error) {
|
|
4011
|
+
return error?.code === "ENOENT";
|
|
4012
|
+
}
|
|
4013
|
+
async function atomicWrite(targetFile, content) {
|
|
4014
|
+
const temporaryFile = `${targetFile}.${process.pid}.${Date.now()}.tmp`;
|
|
4015
|
+
try {
|
|
4016
|
+
await writeFile(temporaryFile, content, { encoding: "utf8", mode: 420, flag: "wx" });
|
|
4017
|
+
await rename(temporaryFile, targetFile);
|
|
4018
|
+
} catch (error) {
|
|
4019
|
+
await unlink(temporaryFile).catch(() => void 0);
|
|
4020
|
+
throw error;
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
async function installCodexSkill(options) {
|
|
4024
|
+
let source;
|
|
4025
|
+
try {
|
|
4026
|
+
source = await readFile2(options.sourceFile, "utf8");
|
|
4027
|
+
} catch (error) {
|
|
4028
|
+
if (isMissing(error)) {
|
|
4029
|
+
throw new Error(`Skill source is missing: ${options.sourceFile}`);
|
|
4030
|
+
}
|
|
4031
|
+
throw error;
|
|
4032
|
+
}
|
|
4033
|
+
const targetDirectory = join3(options.skillsRoot, SKILL_DIRECTORY_NAME);
|
|
4034
|
+
const targetFile = join3(targetDirectory, "SKILL.md");
|
|
4035
|
+
let existing;
|
|
4036
|
+
try {
|
|
4037
|
+
existing = await readFile2(targetFile, "utf8");
|
|
4038
|
+
} catch (error) {
|
|
4039
|
+
if (!isMissing(error)) throw error;
|
|
4040
|
+
}
|
|
4041
|
+
if (existing === source) return { status: "unchanged", targetFile };
|
|
4042
|
+
if (existing !== void 0 && !options.force) {
|
|
4043
|
+
throw new Error(
|
|
4044
|
+
`Codex Skill already exists with different content at ${targetFile}; rerun with --force to overwrite it.`
|
|
4045
|
+
);
|
|
4046
|
+
}
|
|
4047
|
+
await mkdir(targetDirectory, { recursive: true });
|
|
4048
|
+
await atomicWrite(targetFile, source);
|
|
4049
|
+
return {
|
|
4050
|
+
status: existing === void 0 ? "installed" : "overwritten",
|
|
4051
|
+
targetFile
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
function defaultCodexSkillsRoot(env = process.env, homeDirectory = homedir2()) {
|
|
4055
|
+
return env.ATWEBPILOT_CODEX_SKILLS_DIR || join3(homeDirectory, ".agents", "skills");
|
|
4056
|
+
}
|
|
4057
|
+
async function resolveBundledSkillFile(moduleUrl = import.meta.url) {
|
|
4058
|
+
const here = dirname2(fileURLToPath2(moduleUrl));
|
|
4059
|
+
const candidates = [
|
|
4060
|
+
join3(here, "skill", "SKILL.md"),
|
|
4061
|
+
join3(here, "../../../skill/SKILL.md")
|
|
4062
|
+
];
|
|
4063
|
+
for (const candidate of candidates) {
|
|
4064
|
+
try {
|
|
4065
|
+
await access(candidate);
|
|
4066
|
+
return candidate;
|
|
4067
|
+
} catch {
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
throw new Error(`Skill source is missing; checked: ${candidates.join(", ")}`);
|
|
4071
|
+
}
|
|
4072
|
+
async function runSkillInstallCommand(args, overrides = {}) {
|
|
4073
|
+
if (!args.includes("--install-codex-skill")) return { handled: false };
|
|
4074
|
+
const supported = /* @__PURE__ */ new Set(["--install-codex-skill", "--force"]);
|
|
4075
|
+
const unknown = args.filter((arg) => !supported.has(arg));
|
|
4076
|
+
if (unknown.length > 0) {
|
|
4077
|
+
throw new Error(`Unknown option for Skill installation: ${unknown.join(", ")}`);
|
|
4078
|
+
}
|
|
4079
|
+
const sourceFile = overrides.sourceFile ?? await resolveBundledSkillFile();
|
|
4080
|
+
const skillsRoot = overrides.skillsRoot ?? defaultCodexSkillsRoot();
|
|
4081
|
+
const result = await installCodexSkill({
|
|
4082
|
+
sourceFile,
|
|
4083
|
+
skillsRoot,
|
|
4084
|
+
force: args.includes("--force")
|
|
4085
|
+
});
|
|
4086
|
+
return { handled: true, result };
|
|
4087
|
+
}
|
|
4088
|
+
|
|
3606
4089
|
// src/index.ts
|
|
3607
4090
|
async function main() {
|
|
4091
|
+
const args = process.argv.slice(2);
|
|
4092
|
+
const install = await runSkillInstallCommand(args);
|
|
4093
|
+
if (install.handled) {
|
|
4094
|
+
process.stdout.write(`Codex Skill ${install.result.status}: ${install.result.targetFile}
|
|
4095
|
+
`);
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
if (args.length > 0) {
|
|
4099
|
+
throw new Error(`Unknown option: ${args.join(", ")}`);
|
|
4100
|
+
}
|
|
3608
4101
|
const explicitPort = process.env.ATWEBPILOT_WS_PORT ? Number(process.env.ATWEBPILOT_WS_PORT) : void 0;
|
|
3609
4102
|
const token = process.env.ATWEBPILOT_WS_TOKEN || void 0;
|
|
3610
4103
|
const clock = new DefaultClock();
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: atwebpilot-browser
|
|
3
|
+
description: Use AtWebPilot when a task requires inspecting, testing, debugging, screenshotting, reading, or interacting with a live web page in the user's existing Chrome, including localhost and authenticated pages. Do not use it for pure code editing or static source analysis that does not require observing a live page.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# atwebpilot-browser skill
|
|
2
7
|
|
|
3
8
|
A reusable strategy for agents that drive AtWebPilot's browser extension via
|
|
@@ -12,6 +17,10 @@ When this skill is loaded, you can drive any open tab through these tools
|
|
|
12
17
|
|
|
13
18
|
### Control plane
|
|
14
19
|
|
|
20
|
+
- `browser_start({ tab_id?, url? })` — primary entry point for live-page work.
|
|
21
|
+
It pairs when needed and returns a ready session when exactly one tab can be
|
|
22
|
+
selected safely. When it returns `select_tab`, call it again with one listed
|
|
23
|
+
`tab_id`; it never guesses between tabs.
|
|
15
24
|
- `pairing_status` — start/check pairing without waiting. When it reports
|
|
16
25
|
`pairing_required`, show `pair_url` once; use it only as a later status check.
|
|
17
26
|
- `list_tabs` — on first use, open the pairing page and wait up to 45 seconds
|
|
@@ -88,7 +97,7 @@ compatibility path; use the native tool once the host exposes it.
|
|
|
88
97
|
|
|
89
98
|
## Recommended flow
|
|
90
99
|
|
|
91
|
-
1. **先建立连接**:调一次 `
|
|
100
|
+
1. **先建立连接**:调一次 `browser_start`;首次配对时它会打开授权页并等待最多 45 秒,唯一 tab 可确定时同一次调用直接返回 ready session。返回 `select_tab` 时带 `tab_id` 再调一次;只有超时才会返回 `pairing_required`,此时展示 `pair_url`,稍后用非阻塞的 `pairing_status` 检查,不要轮询 `list_tabs`。
|
|
92
101
|
2. **探查先于操作**:每次进入新页面,先 `getPageInfo` 确认位置,再 `takeSnapshot`(要点击/填表)或 `createPageIndex`(要读内容/抽字段);`snapshotDOM` / `querySelector` 属于 legacy-dom 组,只在需要分析整页结构时通过 `browser_discoverTools` 启用。
|
|
93
102
|
3. **最少充分验证**:危险或不可逆操作保持单步;多个独立字段用 `fillForm`。工具已返回 `verified` 或明确后置状态时,不要再用 `runJS` 重复验证。
|
|
94
103
|
4. **dangerous 工具会被人工审核**:调用前用 `extractText` 给用户看上下文,让审批更顺。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@attson/atwebpilot-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.82",
|
|
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",
|