@attson/atwebpilot-mcp 0.0.79 → 0.0.81
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 +5 -4
- package/dist/index.js +391 -32
- package/dist/skill/SKILL.md +11 -9
- package/package.json +1 -1
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` 时才绑一个空闲端口,通过 MCP URL elicitation 请宿主打开配对页;不支持该能力的宿主回退到系统默认浏览器。`list_tabs` 最多等待 45 秒。
|
|
21
|
+
3. 你在浏览器里点「允许」,扩展即接入;同一次 `list_tabs` 调用直接返回 tab,AI 无需让你回复「已连接」。只有等待超时才返回 `pairing_required + pair_url`;`pairing_status` 始终是立即返回的非阻塞状态检查。
|
|
22
22
|
4. 之后本机的会话都免确认(信任是安装级的);端口能复用时连配对页都不会弹。
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
若宿主和系统都未弹出浏览器,可在超时结果中打开 `pair_url`。授权成功只以扩展实际建立连接并发送 `HELLO` 为准;打开配对页本身不代表已连接。
|
|
25
25
|
|
|
26
26
|
多个 Codex / Claude Code 会话可以同时接入同一个浏览器,各自一条连接。可在扩展设置 →
|
|
27
27
|
Coordinator 查看已接入的会话、单独断开,或撤销信任。
|
|
@@ -51,6 +51,7 @@ listen 端口的能力,所以方向只能如此。这也是配对页存在的
|
|
|
51
51
|
- 控制面 5 个:`pairing_status / list_tabs / open_session / close_session / get_quota`
|
|
52
52
|
- 发现 1 个:`browser_discoverTools` —— 已知工具时直接传 `enable: [...]`,已知分组时直接传 `enableGroups: ["export", ...]`,也可用 `profile: "core" | "full"` 切换整套工具面;只有这些都未知时才不带参数读取目录。选中的工具会加入本进程的 `tools/list`(发送 `tools/list_changed`),并始终返回完整 schema,包括已经启用的工具。
|
|
53
53
|
- 同轮执行桥 1 个:`browser_call({session_id, tool, args})` —— 某些宿主虽已刷新 `tools/list`,却不会在当前模型轮次注入新工具。AI 可在 `discoverTools` 后立刻通过该桥执行,不需要让用户回复“继续”;下一轮看到原生工具后仍优先使用原生工具。
|
|
54
|
+
- 批量编排 1 个:`browser_runSteps({session_id, steps, return_bindings?})` —— 在步骤和参数已经明确时,把 1–20 个 safe/caution 工具合并为一次 MCP 调用;`${binding}` 可引用前序结果。整批先做工具支持、权限和配额预检,dangerous、脚本、截图、下载与 tab 生命周期操作保持单步。
|
|
54
55
|
- 执行面在浏览器连接前不进入 `tools/list`;连接确认后加载默认 **core 31 个** `browser_*`。其余 20 个由 AI 按需 `discoverTools` 拉取,用户不用配置。
|
|
55
56
|
- 不暴露 `askUser`(MCP 会话没有人在侧边栏应答)和 `attachTab` / `detachTab`(目标 tab 已由 `open_session` 绑定)。
|
|
56
57
|
|
|
@@ -66,7 +67,7 @@ hover / uploadFile / scroll / waitFor`);`browser_storage` 也不会列出,
|
|
|
66
67
|
|
|
67
68
|
| 值 | 效果 |
|
|
68
69
|
|---|---|
|
|
69
|
-
| `core`(默认) |
|
|
70
|
+
| `core`(默认) | 连接前仅控制面、发现工具、同轮执行桥与批量编排;连接后再列出 31 个核心工具 |
|
|
70
71
|
| `full` | 连接前同上;连接后列出全部 51 个原生工具 |
|
|
71
72
|
|
|
72
73
|
无法识别的值(包括已移除的 `parity`)按 `core` 处理,并往 stderr 打一条提示。
|
package/dist/index.js
CHANGED
|
@@ -953,7 +953,7 @@ var CONTROL_TOOLS = [
|
|
|
953
953
|
},
|
|
954
954
|
{
|
|
955
955
|
name: "list_tabs",
|
|
956
|
-
description: "List browser tabs.
|
|
956
|
+
description: "List browser tabs. On first use, opens the pairing page and waits up to 45 seconds for authorization, normally returning connected tabs in the same call. Returns pairing_required with the URL only on timeout.",
|
|
957
957
|
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
958
958
|
},
|
|
959
959
|
{
|
|
@@ -2325,7 +2325,18 @@ function locate(source, offset) {
|
|
|
2325
2325
|
return { line, col };
|
|
2326
2326
|
}
|
|
2327
2327
|
|
|
2328
|
+
// src/pairing-errors.ts
|
|
2329
|
+
var PairingWaitTimeoutError = class extends Error {
|
|
2330
|
+
constructor(timeoutMs, pairUrl) {
|
|
2331
|
+
super(
|
|
2332
|
+
`\u7B49\u5F85\u6D4F\u89C8\u5668\u6388\u6743\u8D85\u65F6\uFF08${Math.ceil(timeoutMs / 1e3)} \u79D2\uFF09\u3002` + (pairUrl ? `\u8BF7\u6253\u5F00\u914D\u5BF9\u9875 ${pairUrl} \u5B8C\u6210\u6388\u6743\u540E\u91CD\u8BD5\u3002` : "")
|
|
2333
|
+
);
|
|
2334
|
+
this.name = "PairingWaitTimeoutError";
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2328
2338
|
// src/handlers.ts
|
|
2339
|
+
var LIST_TABS_PAIRING_TIMEOUT_MS = 45e3;
|
|
2329
2340
|
function singleWorkerId(c, pairUrl) {
|
|
2330
2341
|
const workers = c.workers.list();
|
|
2331
2342
|
if (workers.length === 0) {
|
|
@@ -2349,17 +2360,32 @@ async function handlePairingStatus(deps) {
|
|
|
2349
2360
|
if (!pairUrl) throw new Error("PAIRING_REQUIRED: \u914D\u5BF9\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u4F46\u914D\u5BF9\u9875\u5C1A\u4E0D\u53EF\u7528");
|
|
2350
2361
|
return { status: "pairing_required", pair_url: pairUrl };
|
|
2351
2362
|
}
|
|
2352
|
-
async function handleListTabs(deps) {
|
|
2363
|
+
async function handleListTabs(deps, onPairingRequired, timeoutMs = LIST_TABS_PAIRING_TIMEOUT_MS) {
|
|
2353
2364
|
const { coordinator } = await deps.ensure();
|
|
2354
2365
|
const workers = coordinator.workers.list();
|
|
2355
2366
|
if (workers.length > 1) {
|
|
2356
2367
|
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");
|
|
2357
2368
|
}
|
|
2358
|
-
|
|
2369
|
+
let worker = workers[0];
|
|
2359
2370
|
if (!worker) {
|
|
2360
2371
|
const pairUrl = deps.pairUrl();
|
|
2361
2372
|
if (!pairUrl) throw new Error("PAIRING_REQUIRED: \u914D\u5BF9\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u4F46\u914D\u5BF9\u9875\u5C1A\u4E0D\u53EF\u7528");
|
|
2362
|
-
|
|
2373
|
+
try {
|
|
2374
|
+
await deps.waitForWorker(timeoutMs, onPairingRequired);
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
if (error instanceof PairingWaitTimeoutError) {
|
|
2377
|
+
return { status: "pairing_required", pair_url: pairUrl, tabs: [] };
|
|
2378
|
+
}
|
|
2379
|
+
throw error;
|
|
2380
|
+
}
|
|
2381
|
+
const connectedWorkers = coordinator.workers.list();
|
|
2382
|
+
if (connectedWorkers.length > 1) {
|
|
2383
|
+
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");
|
|
2384
|
+
}
|
|
2385
|
+
worker = connectedWorkers[0];
|
|
2386
|
+
if (!worker) {
|
|
2387
|
+
return { status: "pairing_required", pair_url: pairUrl, tabs: [] };
|
|
2388
|
+
}
|
|
2363
2389
|
}
|
|
2364
2390
|
return { status: "connected", tabs: worker.available_tabs };
|
|
2365
2391
|
}
|
|
@@ -2388,7 +2414,7 @@ var NON_BUILTIN_CAPABILITY = {
|
|
|
2388
2414
|
listTabs: "tab:open",
|
|
2389
2415
|
openTab: "tab:open"
|
|
2390
2416
|
};
|
|
2391
|
-
async function
|
|
2417
|
+
async function prepareBrowserTool(deps, gen, args) {
|
|
2392
2418
|
const { coordinator, hub } = await deps.ensure();
|
|
2393
2419
|
const session_id = String(args.session_id);
|
|
2394
2420
|
const session = coordinator.sessions.get(session_id);
|
|
@@ -2403,23 +2429,20 @@ async function handleBrowserTool(deps, gen, args) {
|
|
|
2403
2429
|
const unsafe = highestSeverity(runStaticScan(source)) === "dangerous";
|
|
2404
2430
|
const v = coordinator.validateCall({ session_id, kind: "runJS", unsafe });
|
|
2405
2431
|
if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
|
|
2406
|
-
coordinator.recordCall(session_id, v.dangerous);
|
|
2407
2432
|
const requestedTimeout = toolArgs.timeoutMs;
|
|
2408
|
-
|
|
2433
|
+
return {
|
|
2434
|
+
coordinator,
|
|
2435
|
+
hub,
|
|
2409
2436
|
session_id,
|
|
2437
|
+
worker_id: session.worker_id,
|
|
2410
2438
|
tab_id: session.tab_id,
|
|
2411
2439
|
step: {
|
|
2412
2440
|
kind: "js",
|
|
2413
2441
|
source,
|
|
2414
2442
|
...typeof requestedTimeout === "number" && Number.isInteger(requestedTimeout) && requestedTimeout > 0 ? { timeoutMs: requestedTimeout } : {}
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
throw new Error(
|
|
2419
|
-
jsResult.error ? `${jsResult.error.code}: ${jsResult.error.message}` : "EXEC failed"
|
|
2420
|
-
);
|
|
2421
|
-
}
|
|
2422
|
-
return jsResult.return ?? null;
|
|
2443
|
+
},
|
|
2444
|
+
dangerous: v.dangerous
|
|
2445
|
+
};
|
|
2423
2446
|
}
|
|
2424
2447
|
if (tool === "switchToTab" || tool === "closeTab") {
|
|
2425
2448
|
const boundTabId = Number.parseInt(session.tab_id, 10);
|
|
@@ -2429,10 +2452,11 @@ async function handleBrowserTool(deps, gen, args) {
|
|
|
2429
2452
|
toolArgs.tabId = boundTabId;
|
|
2430
2453
|
}
|
|
2431
2454
|
const override = NON_BUILTIN_CAPABILITY[tool];
|
|
2455
|
+
let dangerous;
|
|
2432
2456
|
if (override) {
|
|
2433
2457
|
const v = coordinator.validateCall({ session_id, kind: "capability", capability: override });
|
|
2434
2458
|
if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
|
|
2435
|
-
|
|
2459
|
+
dangerous = v.dangerous;
|
|
2436
2460
|
} else {
|
|
2437
2461
|
const raw = toolArgs;
|
|
2438
2462
|
const v = coordinator.validateCall({
|
|
@@ -2444,13 +2468,321 @@ async function handleBrowserTool(deps, gen, args) {
|
|
|
2444
2468
|
recorderArmsBodies: tool === "recorderConfig" ? raw.bodies === true : void 0
|
|
2445
2469
|
});
|
|
2446
2470
|
if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
|
|
2447
|
-
|
|
2471
|
+
dangerous = v.dangerous;
|
|
2448
2472
|
}
|
|
2449
|
-
|
|
2473
|
+
return {
|
|
2474
|
+
coordinator,
|
|
2475
|
+
hub,
|
|
2476
|
+
session_id,
|
|
2477
|
+
worker_id: session.worker_id,
|
|
2478
|
+
tab_id: session.tab_id,
|
|
2479
|
+
step: { kind: "tool", tool, args: toolArgs },
|
|
2480
|
+
dangerous
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
async function handleBrowserTool(deps, gen, args) {
|
|
2484
|
+
const prepared = await prepareBrowserTool(deps, gen, args);
|
|
2485
|
+
prepared.coordinator.recordCall(prepared.session_id, prepared.dangerous);
|
|
2486
|
+
const result = await prepared.hub.exec(prepared.worker_id, {
|
|
2487
|
+
session_id: prepared.session_id,
|
|
2488
|
+
tab_id: prepared.tab_id,
|
|
2489
|
+
step: prepared.step
|
|
2490
|
+
});
|
|
2450
2491
|
if (!result.ok) throw new Error(result.error ? `${result.error.code}: ${result.error.message}` : "EXEC failed");
|
|
2451
2492
|
return result.return ?? null;
|
|
2452
2493
|
}
|
|
2453
2494
|
|
|
2495
|
+
// src/browser-run-steps.ts
|
|
2496
|
+
var BROWSER_RUN_STEPS_TOOL = {
|
|
2497
|
+
name: "browser_runSteps",
|
|
2498
|
+
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.",
|
|
2499
|
+
inputSchema: {
|
|
2500
|
+
type: "object",
|
|
2501
|
+
required: ["session_id", "steps"],
|
|
2502
|
+
properties: {
|
|
2503
|
+
session_id: { type: "string", description: "Session id from open_session" },
|
|
2504
|
+
steps: {
|
|
2505
|
+
type: "array",
|
|
2506
|
+
minItems: 1,
|
|
2507
|
+
maxItems: 20,
|
|
2508
|
+
items: {
|
|
2509
|
+
type: "object",
|
|
2510
|
+
required: ["tool", "args"],
|
|
2511
|
+
properties: {
|
|
2512
|
+
tool: {
|
|
2513
|
+
type: "string",
|
|
2514
|
+
description: "Enabled browser tool name; browser_ prefix is optional"
|
|
2515
|
+
},
|
|
2516
|
+
args: {
|
|
2517
|
+
type: "object",
|
|
2518
|
+
description: "Tool arguments excluding session_id; may reference earlier ${bindings}",
|
|
2519
|
+
additionalProperties: true
|
|
2520
|
+
},
|
|
2521
|
+
bind_result_to: {
|
|
2522
|
+
type: "string",
|
|
2523
|
+
pattern: "^[A-Za-z_][A-Za-z0-9_]{0,63}$"
|
|
2524
|
+
}
|
|
2525
|
+
},
|
|
2526
|
+
additionalProperties: false
|
|
2527
|
+
}
|
|
2528
|
+
},
|
|
2529
|
+
return_bindings: {
|
|
2530
|
+
type: "array",
|
|
2531
|
+
items: {
|
|
2532
|
+
type: "string",
|
|
2533
|
+
pattern: "^[A-Za-z_][A-Za-z0-9_]{0,63}$"
|
|
2534
|
+
},
|
|
2535
|
+
uniqueItems: true
|
|
2536
|
+
}
|
|
2537
|
+
},
|
|
2538
|
+
additionalProperties: false
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
2541
|
+
var BINDING_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
2542
|
+
var BLOCKED_BATCH_TOOLS = /* @__PURE__ */ new Set([
|
|
2543
|
+
"browser_downloadImage",
|
|
2544
|
+
"browser_downloadSpreadsheet",
|
|
2545
|
+
"browser_openTab",
|
|
2546
|
+
"browser_closeTab",
|
|
2547
|
+
"browser_switchToTab"
|
|
2548
|
+
]);
|
|
2549
|
+
var CAPABILITY_SENSITIVE_FIELDS = /* @__PURE__ */ new Map([
|
|
2550
|
+
["browser_httpRequest", ["withCredentials"]],
|
|
2551
|
+
["browser_drop", ["files"]],
|
|
2552
|
+
["browser_recorderConfig", ["bodies"]]
|
|
2553
|
+
]);
|
|
2554
|
+
function runStepsPreflightError(message) {
|
|
2555
|
+
return {
|
|
2556
|
+
ok: false,
|
|
2557
|
+
phase: "preflight",
|
|
2558
|
+
completed: 0,
|
|
2559
|
+
error: { message },
|
|
2560
|
+
bindings: {}
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
async function handleBrowserRunSteps(deps, args, context) {
|
|
2564
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id : "";
|
|
2565
|
+
if (!sessionId) throw new Error("InvalidArgs: browser_runSteps requires session_id");
|
|
2566
|
+
if (!Array.isArray(args.steps) || args.steps.length < 1 || args.steps.length > 20) {
|
|
2567
|
+
throw new Error("InvalidArgs: browser_runSteps requires 1-20 steps");
|
|
2568
|
+
}
|
|
2569
|
+
const steps = args.steps.map(parseStep);
|
|
2570
|
+
const requested = parseReturnBindings(args.return_bindings);
|
|
2571
|
+
const bindings = /* @__PURE__ */ Object.create(null);
|
|
2572
|
+
let result = null;
|
|
2573
|
+
const bindingIssue = validateBindingPlan(steps, requested);
|
|
2574
|
+
if (bindingIssue) {
|
|
2575
|
+
return preflightFailure(steps, bindingIssue.index, bindingIssue.message);
|
|
2576
|
+
}
|
|
2577
|
+
const { coordinator } = await deps.ensure();
|
|
2578
|
+
const quota = coordinator.quotaFor(sessionId);
|
|
2579
|
+
if (!quota) throw new Error(`session ${sessionId} not found`);
|
|
2580
|
+
const remainingSteps = quota.max_steps - quota.steps_used;
|
|
2581
|
+
if (steps.length > remainingSteps) {
|
|
2582
|
+
const failedIndex = Math.max(0, remainingSteps);
|
|
2583
|
+
return preflightFailure(
|
|
2584
|
+
steps,
|
|
2585
|
+
failedIndex,
|
|
2586
|
+
`Session has only ${remainingSteps} steps remaining; batch requires ${steps.length}`
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
for (let index = 0; index < steps.length; index++) {
|
|
2590
|
+
const step = steps[index];
|
|
2591
|
+
const name = browserToolName(step.tool);
|
|
2592
|
+
const generated = context.tools.get(name);
|
|
2593
|
+
try {
|
|
2594
|
+
assertAvailable(name, step.tool, generated, context);
|
|
2595
|
+
if (generated.stepKind === "js" || generated.resultKind === "image" || BLOCKED_BATCH_TOOLS.has(name)) {
|
|
2596
|
+
throw new Error(`Tool ${name} is not allowed in browser_runSteps`);
|
|
2597
|
+
}
|
|
2598
|
+
for (const field of CAPABILITY_SENSITIVE_FIELDS.get(name) ?? []) {
|
|
2599
|
+
if (bindingReferences(step.args[field]).length > 0) {
|
|
2600
|
+
throw new Error(
|
|
2601
|
+
`Capability-sensitive field ${field} cannot reference bindings in browser_runSteps`
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const prepared = await prepareBrowserTool(deps, generated, {
|
|
2606
|
+
...step.args,
|
|
2607
|
+
session_id: sessionId
|
|
2608
|
+
});
|
|
2609
|
+
if (prepared.dangerous) {
|
|
2610
|
+
throw new Error("Dangerous tool is not allowed in browser_runSteps");
|
|
2611
|
+
}
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
return {
|
|
2614
|
+
ok: false,
|
|
2615
|
+
phase: "preflight",
|
|
2616
|
+
completed: 0,
|
|
2617
|
+
failed_step: { index, tool: name },
|
|
2618
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
2619
|
+
bindings: {}
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
for (let index = 0; index < steps.length; index++) {
|
|
2624
|
+
const step = steps[index];
|
|
2625
|
+
const name = browserToolName(step.tool);
|
|
2626
|
+
const generated = context.tools.get(name);
|
|
2627
|
+
assertAvailable(name, step.tool, generated, context);
|
|
2628
|
+
const resolvedArgs = resolveBindings(step.args, bindings);
|
|
2629
|
+
try {
|
|
2630
|
+
result = await handleBrowserTool(deps, generated, { ...resolvedArgs, session_id: sessionId });
|
|
2631
|
+
} catch (error) {
|
|
2632
|
+
return {
|
|
2633
|
+
ok: false,
|
|
2634
|
+
phase: "execution",
|
|
2635
|
+
completed: index,
|
|
2636
|
+
failed_step: { index, tool: name },
|
|
2637
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
2638
|
+
bindings: selectBindings(requested, bindings, false)
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
if (step.bind_result_to) bindings[step.bind_result_to] = result;
|
|
2642
|
+
}
|
|
2643
|
+
return {
|
|
2644
|
+
ok: true,
|
|
2645
|
+
completed: steps.length,
|
|
2646
|
+
result,
|
|
2647
|
+
bindings: selectBindings(requested, bindings, true)
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
function preflightFailure(steps, index, message) {
|
|
2651
|
+
const boundedIndex = Math.min(Math.max(index, 0), steps.length - 1);
|
|
2652
|
+
return {
|
|
2653
|
+
ok: false,
|
|
2654
|
+
phase: "preflight",
|
|
2655
|
+
completed: 0,
|
|
2656
|
+
failed_step: {
|
|
2657
|
+
index: boundedIndex,
|
|
2658
|
+
tool: browserToolName(steps[boundedIndex].tool)
|
|
2659
|
+
},
|
|
2660
|
+
error: { message },
|
|
2661
|
+
bindings: {}
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
function parseReturnBindings(value) {
|
|
2665
|
+
if (value === void 0) return [];
|
|
2666
|
+
if (!Array.isArray(value) || value.some((name) => typeof name !== "string")) {
|
|
2667
|
+
throw new Error("InvalidArgs: return_bindings must be an array of binding names");
|
|
2668
|
+
}
|
|
2669
|
+
return value;
|
|
2670
|
+
}
|
|
2671
|
+
function validateBindingPlan(steps, requested) {
|
|
2672
|
+
const declared = /* @__PURE__ */ new Set();
|
|
2673
|
+
for (let index = 0; index < steps.length; index++) {
|
|
2674
|
+
const step = steps[index];
|
|
2675
|
+
for (const name of bindingReferences(step.args)) {
|
|
2676
|
+
if (!declared.has(name)) {
|
|
2677
|
+
return {
|
|
2678
|
+
index,
|
|
2679
|
+
message: `InvalidArgs: binding ${name} must be declared by an earlier step`
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
if (step.bind_result_to) {
|
|
2684
|
+
if (!BINDING_NAME.test(step.bind_result_to)) {
|
|
2685
|
+
return { index, message: `InvalidArgs: invalid binding name ${step.bind_result_to}` };
|
|
2686
|
+
}
|
|
2687
|
+
if (declared.has(step.bind_result_to)) {
|
|
2688
|
+
return { index, message: `InvalidArgs: duplicate binding ${step.bind_result_to}` };
|
|
2689
|
+
}
|
|
2690
|
+
declared.add(step.bind_result_to);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
const seenRequested = /* @__PURE__ */ new Set();
|
|
2694
|
+
for (const name of requested) {
|
|
2695
|
+
if (!BINDING_NAME.test(name)) {
|
|
2696
|
+
return { index: 0, message: `InvalidArgs: invalid return binding ${name}` };
|
|
2697
|
+
}
|
|
2698
|
+
if (seenRequested.has(name)) {
|
|
2699
|
+
return { index: 0, message: `InvalidArgs: duplicate return binding ${name}` };
|
|
2700
|
+
}
|
|
2701
|
+
if (!declared.has(name)) {
|
|
2702
|
+
return { index: 0, message: `InvalidArgs: unknown return binding ${name}` };
|
|
2703
|
+
}
|
|
2704
|
+
seenRequested.add(name);
|
|
2705
|
+
}
|
|
2706
|
+
return null;
|
|
2707
|
+
}
|
|
2708
|
+
function bindingReferences(value) {
|
|
2709
|
+
if (typeof value === "string") {
|
|
2710
|
+
return [...value.matchAll(/\$\{([^}]+)\}/g)].map((match) => match[1]);
|
|
2711
|
+
}
|
|
2712
|
+
if (Array.isArray(value)) return value.flatMap(bindingReferences);
|
|
2713
|
+
if (value && typeof value === "object") {
|
|
2714
|
+
return Object.values(value).flatMap(bindingReferences);
|
|
2715
|
+
}
|
|
2716
|
+
return [];
|
|
2717
|
+
}
|
|
2718
|
+
function assertAvailable(name, rawName, generated, context) {
|
|
2719
|
+
if (!generated) throw new Error(`Unknown browser tool: ${rawName}`);
|
|
2720
|
+
if (!context.advertised.has(name)) {
|
|
2721
|
+
throw new Error(`${name} is not enabled; call browser_discoverTools first`);
|
|
2722
|
+
}
|
|
2723
|
+
if (context.supported && !generated.builtinTools.every((tool) => context.supported.has(tool))) {
|
|
2724
|
+
throw new Error(`${name} is not supported by the connected browser extension`);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
function parseStep(value) {
|
|
2728
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2729
|
+
throw new Error("InvalidArgs: each browser_runSteps step must be an object");
|
|
2730
|
+
}
|
|
2731
|
+
const raw = value;
|
|
2732
|
+
if (typeof raw.tool !== "string" || raw.tool.trim() === "") {
|
|
2733
|
+
throw new Error("InvalidArgs: each browser_runSteps step requires tool");
|
|
2734
|
+
}
|
|
2735
|
+
if (!raw.args || typeof raw.args !== "object" || Array.isArray(raw.args)) {
|
|
2736
|
+
throw new Error("InvalidArgs: each browser_runSteps step requires object args");
|
|
2737
|
+
}
|
|
2738
|
+
if (raw.bind_result_to !== void 0 && typeof raw.bind_result_to !== "string") {
|
|
2739
|
+
throw new Error("InvalidArgs: bind_result_to must be a binding name");
|
|
2740
|
+
}
|
|
2741
|
+
return {
|
|
2742
|
+
tool: raw.tool,
|
|
2743
|
+
args: raw.args,
|
|
2744
|
+
...typeof raw.bind_result_to === "string" ? { bind_result_to: raw.bind_result_to } : {}
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function browserToolName(raw) {
|
|
2748
|
+
const name = raw.trim();
|
|
2749
|
+
return name.startsWith("browser_") ? name : `browser_${name}`;
|
|
2750
|
+
}
|
|
2751
|
+
function resolveBindings(value, bindings) {
|
|
2752
|
+
if (typeof value === "string") {
|
|
2753
|
+
const exact = value.match(/^\$\{([^}]+)\}$/);
|
|
2754
|
+
if (exact) return requireBinding(exact[1], bindings);
|
|
2755
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
|
|
2756
|
+
const bound = requireBinding(name, bindings);
|
|
2757
|
+
return typeof bound === "string" ? bound : JSON.stringify(bound);
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
if (Array.isArray(value)) return value.map((item) => resolveBindings(item, bindings));
|
|
2761
|
+
if (value && typeof value === "object") {
|
|
2762
|
+
const resolved = {};
|
|
2763
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2764
|
+
resolved[key] = resolveBindings(item, bindings);
|
|
2765
|
+
}
|
|
2766
|
+
return resolved;
|
|
2767
|
+
}
|
|
2768
|
+
return value;
|
|
2769
|
+
}
|
|
2770
|
+
function requireBinding(name, bindings) {
|
|
2771
|
+
if (!(name in bindings)) throw new Error(`InvalidArgs: binding ${name} is not available`);
|
|
2772
|
+
return bindings[name];
|
|
2773
|
+
}
|
|
2774
|
+
function selectBindings(requested, bindings, requireAll) {
|
|
2775
|
+
const selected = {};
|
|
2776
|
+
for (const name of requested) {
|
|
2777
|
+
if (!(name in bindings)) {
|
|
2778
|
+
if (requireAll) throw new Error(`InvalidArgs: unknown return binding ${name}`);
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
selected[name] = bindings[name];
|
|
2782
|
+
}
|
|
2783
|
+
return selected;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2454
2786
|
// src/skill-bundle.ts
|
|
2455
2787
|
import { readFileSync } from "fs";
|
|
2456
2788
|
import { dirname, join } from "path";
|
|
@@ -2460,16 +2792,17 @@ var SKILL_DESCRIPTION = "Strategy + scenarios + safety rails for driving the AtW
|
|
|
2460
2792
|
var TOPICS = {
|
|
2461
2793
|
quickstart: `# AtWebPilot quickstart
|
|
2462
2794
|
|
|
2463
|
-
1. Call list_tabs once.
|
|
2464
|
-
2.
|
|
2465
|
-
3.
|
|
2795
|
+
1. Call list_tabs once. On first use it opens the pairing page and waits for authorization, then normally returns connected tabs in the same call.
|
|
2796
|
+
2. If list_tabs 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.
|
|
2797
|
+
3. Choose a returned tab, then call open_session with its tab_id.
|
|
2466
2798
|
4. For interaction use browser_getPageInfo then browser_takeSnapshot; for broad reading use browser_createPageIndex.
|
|
2467
|
-
5.
|
|
2468
|
-
6.
|
|
2799
|
+
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.
|
|
2800
|
+
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".
|
|
2801
|
+
7. Close the session when finished.
|
|
2469
2802
|
`,
|
|
2470
2803
|
pairing: `# AtWebPilot pairing
|
|
2471
2804
|
|
|
2472
|
-
list_tabs and
|
|
2805
|
+
list_tabs opens the pairing page when needed and waits up to 45 seconds for the extension's HELLO, so successful authorization normally returns connected tabs in the same call. pairing_status remains immediate and non-blocking. A pairing_required result from list_tabs means the bounded wait expired; show pair_url once and use pairing_status for a later check. Never ask the user to reply merely to continue, poll list_tabs, or inspect the pair-page HTML.
|
|
2473
2806
|
`,
|
|
2474
2807
|
reading: `# AtWebPilot reading
|
|
2475
2808
|
|
|
@@ -2511,9 +2844,11 @@ function readSkillBundle(rawTopic = "quickstart") {
|
|
|
2511
2844
|
}
|
|
2512
2845
|
if (cached == null) {
|
|
2513
2846
|
try {
|
|
2514
|
-
cached =
|
|
2847
|
+
cached = `${TOPICS.quickstart}
|
|
2848
|
+
${readFileSync(locateSkillFile(), "utf-8")}`;
|
|
2515
2849
|
} catch {
|
|
2516
|
-
cached =
|
|
2850
|
+
cached = `${TOPICS.quickstart}
|
|
2851
|
+
# ${SKILL_NAME}
|
|
2517
2852
|
|
|
2518
2853
|
${SKILL_DESCRIPTION}
|
|
2519
2854
|
|
|
@@ -2543,7 +2878,7 @@ var SKILL_TOOL = {
|
|
|
2543
2878
|
// package.json
|
|
2544
2879
|
var package_default = {
|
|
2545
2880
|
name: "@attson/atwebpilot-mcp",
|
|
2546
|
-
version: "0.0.
|
|
2881
|
+
version: "0.0.81",
|
|
2547
2882
|
license: "Apache-2.0",
|
|
2548
2883
|
description: "MCP server for the atwebpilot browser extension - let Codex or Claude Code drive your browser.",
|
|
2549
2884
|
homepage: "https://github.com/attson/atwebpilot#readme",
|
|
@@ -2698,6 +3033,7 @@ function buildToolList(deps, state = DEFAULT_STATE) {
|
|
|
2698
3033
|
...CONTROL_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
2699
3034
|
{ name: DISCOVER_TOOL.name, description: DISCOVER_TOOL.description, inputSchema: DISCOVER_TOOL.inputSchema },
|
|
2700
3035
|
{ name: BROWSER_CALL_TOOL.name, description: BROWSER_CALL_TOOL.description, inputSchema: BROWSER_CALL_TOOL.inputSchema },
|
|
3036
|
+
{ name: BROWSER_RUN_STEPS_TOOL.name, description: BROWSER_RUN_STEPS_TOOL.description, inputSchema: BROWSER_RUN_STEPS_TOOL.inputSchema },
|
|
2701
3037
|
...browser.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
|
|
2702
3038
|
];
|
|
2703
3039
|
}
|
|
@@ -2737,6 +3073,24 @@ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChang
|
|
|
2737
3073
|
if (name === BROWSER_CALL_TOOL.name) {
|
|
2738
3074
|
return handleBrowserCall(deps, args, state);
|
|
2739
3075
|
}
|
|
3076
|
+
if (name === BROWSER_RUN_STEPS_TOOL.name) {
|
|
3077
|
+
let result;
|
|
3078
|
+
try {
|
|
3079
|
+
result = await handleBrowserRunSteps(deps, args, {
|
|
3080
|
+
tools: BROWSER_BY_NAME,
|
|
3081
|
+
advertised: state.advertised,
|
|
3082
|
+
supported: workerToolSupport(deps)
|
|
3083
|
+
});
|
|
3084
|
+
} catch (error) {
|
|
3085
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3086
|
+
if (message.includes("PAIRING_REQUIRED")) {
|
|
3087
|
+
const pairUrl = deps.pairUrl();
|
|
3088
|
+
if (pairUrl) onPairingRequired?.(pairUrl);
|
|
3089
|
+
}
|
|
3090
|
+
result = runStepsPreflightError(message);
|
|
3091
|
+
}
|
|
3092
|
+
return result.ok ? ok(result) : { ...ok(result), isError: true };
|
|
3093
|
+
}
|
|
2740
3094
|
if (name === "pairing_status") {
|
|
2741
3095
|
const result = await handlePairingStatus(deps);
|
|
2742
3096
|
if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
|
|
@@ -2744,9 +3098,8 @@ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChang
|
|
|
2744
3098
|
return ok(result);
|
|
2745
3099
|
}
|
|
2746
3100
|
if (name === "list_tabs") {
|
|
2747
|
-
const result = await handleListTabs(deps);
|
|
3101
|
+
const result = await handleListTabs(deps, onPairingRequired);
|
|
2748
3102
|
if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
|
|
2749
|
-
else onPairingRequired?.(result.pair_url);
|
|
2750
3103
|
return ok(result);
|
|
2751
3104
|
}
|
|
2752
3105
|
if (name === "open_session") {
|
|
@@ -3440,6 +3793,14 @@ function createHubEnsurer(d) {
|
|
|
3440
3793
|
);
|
|
3441
3794
|
}
|
|
3442
3795
|
if (pairUrlValue) await onWaiting?.(pairUrlValue);
|
|
3796
|
+
const connectedAfterPresentation = connectedWorkerId(coordinator);
|
|
3797
|
+
if (connectedAfterPresentation) return connectedAfterPresentation;
|
|
3798
|
+
if (pendingDenial) {
|
|
3799
|
+
pendingDenial = false;
|
|
3800
|
+
throw new Error(
|
|
3801
|
+
"\u7528\u6237\u62D2\u7EDD\u4E86\u6D4F\u89C8\u5668\u914D\u5BF9\u6388\u6743\u3002" + (pairUrlValue ? `\u914D\u5BF9\u9875\uFF1A${pairUrlValue}` : "")
|
|
3802
|
+
);
|
|
3803
|
+
}
|
|
3443
3804
|
if (workerWait) return workerWait.promise;
|
|
3444
3805
|
let resolve;
|
|
3445
3806
|
let reject;
|
|
@@ -3449,9 +3810,7 @@ function createHubEnsurer(d) {
|
|
|
3449
3810
|
});
|
|
3450
3811
|
const timer = setTimeout(() => {
|
|
3451
3812
|
finishWorkerWait({
|
|
3452
|
-
error: new
|
|
3453
|
-
`\u7B49\u5F85\u6D4F\u89C8\u5668\u6388\u6743\u8D85\u65F6\uFF08${Math.ceil(timeoutMs / 1e3)} \u79D2\uFF09\u3002` + (pairUrlValue ? `\u8BF7\u6253\u5F00\u914D\u5BF9\u9875 ${pairUrlValue} \u5B8C\u6210\u6388\u6743\u540E\u91CD\u8BD5\u3002` : "")
|
|
3454
|
-
)
|
|
3813
|
+
error: new PairingWaitTimeoutError(timeoutMs, pairUrlValue)
|
|
3455
3814
|
});
|
|
3456
3815
|
}, timeoutMs);
|
|
3457
3816
|
timer.unref?.();
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -13,8 +13,9 @@ When this skill is loaded, you can drive any open tab through these tools
|
|
|
13
13
|
### Control plane
|
|
14
14
|
|
|
15
15
|
- `pairing_status` — start/check pairing without waiting. When it reports
|
|
16
|
-
`pairing_required`, show `pair_url` once
|
|
17
|
-
- `list_tabs` —
|
|
16
|
+
`pairing_required`, show `pair_url` once; use it only as a later status check.
|
|
17
|
+
- `list_tabs` — on first use, open the pairing page and wait up to 45 seconds
|
|
18
|
+
for browser authorization, normally returning tabs from the same call. Entries carry `busy`
|
|
18
19
|
and `busy_label` when **another** Claude Code session is already driving that
|
|
19
20
|
tab, and `mine` for tabs you hold. This is advisory — nothing blocks you — but
|
|
20
21
|
contending for a tab another agent is typing into is rarely what you want.
|
|
@@ -87,7 +88,7 @@ compatibility path; use the native tool once the host exposes it.
|
|
|
87
88
|
|
|
88
89
|
## Recommended flow
|
|
89
90
|
|
|
90
|
-
1. **先建立连接**:调一次 `list_tabs
|
|
91
|
+
1. **先建立连接**:调一次 `list_tabs`;首次配对时它会打开授权页并等待最多 45 秒,授权成功后同一次调用直接返回 tab,不要让用户回复「已连接」。只有超时才会返回 `pairing_required`;此时展示 `pair_url`,稍后用非阻塞的 `pairing_status` 检查,不要轮询 `list_tabs`。
|
|
91
92
|
2. **探查先于操作**:每次进入新页面,先 `getPageInfo` 确认位置,再 `takeSnapshot`(要点击/填表)或 `createPageIndex`(要读内容/抽字段);`snapshotDOM` / `querySelector` 属于 legacy-dom 组,只在需要分析整页结构时通过 `browser_discoverTools` 启用。
|
|
92
93
|
3. **最少充分验证**:危险或不可逆操作保持单步;多个独立字段用 `fillForm`。工具已返回 `verified` 或明确后置状态时,不要再用 `runJS` 重复验证。
|
|
93
94
|
4. **dangerous 工具会被人工审核**:调用前用 `extractText` 给用户看上下文,让审批更顺。
|
|
@@ -260,12 +261,13 @@ not the whole history.
|
|
|
260
261
|
Several Claude Code sessions can be attached to the same browser at once, each
|
|
261
262
|
through its own connection. Two consequences worth knowing:
|
|
262
263
|
|
|
263
|
-
- **Pairing
|
|
264
|
-
confirmation page,
|
|
265
|
-
`
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
Once approved, later sessions on that
|
|
264
|
+
- **Pairing continues in the same call.** `list_tabs` lazily binds the port,
|
|
265
|
+
opens a confirmation page, and waits up to 45 seconds for the extension's
|
|
266
|
+
`HELLO`. Successful approval returns connected tabs directly; do not ask the
|
|
267
|
+
user to reply just to continue. On timeout it returns `pairing_required` with
|
|
268
|
+
`pair_url`; use non-blocking `pairing_status` for a later check. Never inspect
|
|
269
|
+
the pair-page HTML or poll `list_tabs`. Once approved, later sessions on that
|
|
270
|
+
machine connect silently.
|
|
269
271
|
- **Pick a free tab.** `list_tabs` marks tabs held by other sessions as `busy`.
|
|
270
272
|
Prefer an unowned tab, or open your own — two agents typing into one form
|
|
271
273
|
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.81",
|
|
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",
|