@attson/atwebpilot-mcp 0.0.67 → 0.0.68
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 +3 -1
- package/dist/index.js +134 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,9 +18,11 @@ Claude Code:
|
|
|
18
18
|
|
|
19
19
|
1. 会话启动时**不绑任何端口** —— 不碰网页的会话零副作用。
|
|
20
20
|
2. AI 第一次调 `list_tabs` / `browser_*` 时才绑一个空闲端口,并自动打开配对页。
|
|
21
|
-
3.
|
|
21
|
+
3. 你在浏览器里点「允许」,扩展即接入;正在等待的调用会自动继续,无需告诉 AI 授权成功或手动重试。
|
|
22
22
|
4. 之后本机的会话都免确认(信任是安装级的);端口能复用时连配对页都不会弹。
|
|
23
23
|
|
|
24
|
+
首次调用默认等待授权 90 秒。拒绝或超时会返回明确错误和配对页地址;授权成功只以扩展实际建立连接并发送 `HELLO` 为准。
|
|
25
|
+
|
|
24
26
|
多个 Codex / Claude Code 会话可以同时接入同一个浏览器,各自一条连接。可在扩展设置 →
|
|
25
27
|
Coordinator 查看已接入的会话、单独断开,或撤销信任。
|
|
26
28
|
|
package/dist/index.js
CHANGED
|
@@ -1993,24 +1993,16 @@ function locate(source, offset) {
|
|
|
1993
1993
|
}
|
|
1994
1994
|
|
|
1995
1995
|
// src/handlers.ts
|
|
1996
|
-
function singleWorkerId(c, pairUrl) {
|
|
1997
|
-
const workers = c.workers.list();
|
|
1998
|
-
if (workers.length === 0) {
|
|
1999
|
-
throw new Error(
|
|
2000
|
-
"\u6CA1\u6709\u6D4F\u89C8\u5668\u8FDE\u5165\u3002\u5DF2\u4E3A\u4F60\u6253\u5F00\u914D\u5BF9\u9875" + (pairUrl ? ` ${pairUrl}` : "") + "\uFF0C\u5728\u6D4F\u89C8\u5668\u91CC\u786E\u8BA4\u540E\u91CD\u8BD5\u672C\u6B21\u8C03\u7528\u3002\uFF08\u5982\u679C\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E0D\u662F\u88C5\u4E86\u6269\u5C55\u7684\u90A3\u4E2A\uFF0C\u8BF7\u628A\u8BE5\u5730\u5740\u7C98\u8D34\u8FC7\u53BB\u6253\u5F00\uFF09"
|
|
2001
|
-
);
|
|
2002
|
-
}
|
|
2003
|
-
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");
|
|
2004
|
-
return workers[0].id;
|
|
2005
|
-
}
|
|
2006
1996
|
async function handleListTabs(deps) {
|
|
2007
1997
|
const { coordinator } = await deps.ensure();
|
|
2008
|
-
const
|
|
1998
|
+
const workerId = await deps.waitForWorker();
|
|
1999
|
+
const w = coordinator.workers.get(workerId);
|
|
2000
|
+
if (!w) throw new Error(`worker ${workerId} disconnected while opening browser context`);
|
|
2009
2001
|
return { tabs: w.available_tabs };
|
|
2010
2002
|
}
|
|
2011
2003
|
async function handleOpenSession(deps, args) {
|
|
2012
2004
|
const { coordinator } = await deps.ensure();
|
|
2013
|
-
const worker_id =
|
|
2005
|
+
const worker_id = await deps.waitForWorker();
|
|
2014
2006
|
const tab_id = String(args.tab_id);
|
|
2015
2007
|
const requested = Array.isArray(args.capabilities) ? args.capabilities.map(String).filter(isCapability) : [];
|
|
2016
2008
|
const scope = new Set(requested.length ? requested : CAPABILITIES);
|
|
@@ -2218,7 +2210,9 @@ function createMcpServer(deps) {
|
|
|
2218
2210
|
|
|
2219
2211
|
// src/loopback-ws-hub.ts
|
|
2220
2212
|
import { WebSocketServer, WebSocket } from "ws";
|
|
2221
|
-
import {
|
|
2213
|
+
import {
|
|
2214
|
+
createServer
|
|
2215
|
+
} from "http";
|
|
2222
2216
|
|
|
2223
2217
|
// ../shared/src/pairing/types.ts
|
|
2224
2218
|
var GRACEFUL_CLOSE_CODE = 4e3;
|
|
@@ -2256,6 +2250,17 @@ function renderPairPage(payload) {
|
|
|
2256
2250
|
el.textContent = e.data.trusted ? "\u5DF2\u4FE1\u4EFB\uFF0C\u8FDE\u63A5\u4E2D\u2026" : "\u5DF2\u8FDE\u63A5";
|
|
2257
2251
|
setTimeout(function () { window.close(); }, 1200);
|
|
2258
2252
|
} else if (e.data.reason === "denied") {
|
|
2253
|
+
fetch("/pair/decision", {
|
|
2254
|
+
method: "POST",
|
|
2255
|
+
headers: { "content-type": "application/json" },
|
|
2256
|
+
body: JSON.stringify({
|
|
2257
|
+
v: 1,
|
|
2258
|
+
decision: "denied",
|
|
2259
|
+
sessionId: payload.sessionId,
|
|
2260
|
+
installId: payload.installId,
|
|
2261
|
+
secret: payload.secret
|
|
2262
|
+
})
|
|
2263
|
+
}).catch(function () {});
|
|
2259
2264
|
el.textContent = "\u5DF2\u62D2\u7EDD\u3002\u53EF\u4EE5\u5173\u95ED\u672C\u9875\u3002";
|
|
2260
2265
|
} else {
|
|
2261
2266
|
el.textContent = "\u8FDE\u63A5\u6269\u5C55\u5931\u8D25\u3002\u8BF7\u5237\u65B0\u9875\u9762\u91CD\u8BD5\uFF0C\u6216\u68C0\u67E5\u6269\u5C55\u662F\u5426\u5DF2\u542F\u7528\u3002";
|
|
@@ -2327,12 +2332,17 @@ var LoopbackWSHub = class {
|
|
|
2327
2332
|
execTimeoutMs;
|
|
2328
2333
|
keepaliveIntervalMs;
|
|
2329
2334
|
keepaliveTimers = /* @__PURE__ */ new Map();
|
|
2335
|
+
closedNotified = false;
|
|
2330
2336
|
ready() {
|
|
2331
2337
|
return this.readyPromise;
|
|
2332
2338
|
}
|
|
2333
2339
|
/** Serves the pairing page; everything else is a 404. */
|
|
2334
2340
|
onHttpRequest(req, res) {
|
|
2335
2341
|
const path = (req.url ?? "").split("?")[0];
|
|
2342
|
+
if (path === "/pair/decision" && req.method === "POST") {
|
|
2343
|
+
void this.onPairDecision(req, res);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2336
2346
|
if (path !== "/pair") {
|
|
2337
2347
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
2338
2348
|
res.end("not found");
|
|
@@ -2350,6 +2360,29 @@ var LoopbackWSHub = class {
|
|
|
2350
2360
|
});
|
|
2351
2361
|
res.end(renderPairPage(payload));
|
|
2352
2362
|
}
|
|
2363
|
+
async onPairDecision(req, res) {
|
|
2364
|
+
try {
|
|
2365
|
+
let body = "";
|
|
2366
|
+
for await (const chunk of req) {
|
|
2367
|
+
body += chunk.toString();
|
|
2368
|
+
if (body.length > 4096) throw new Error("pairing decision too large");
|
|
2369
|
+
}
|
|
2370
|
+
const decision = JSON.parse(body);
|
|
2371
|
+
const payload = this.opts.pairPayload?.();
|
|
2372
|
+
const matches = payload && decision.v === 1 && decision.decision === "denied" && decision.sessionId === payload.sessionId && decision.installId === payload.installId && decision.secret === payload.secret;
|
|
2373
|
+
if (!matches) {
|
|
2374
|
+
res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
|
|
2375
|
+
res.end("pairing decision rejected");
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
this.opts.onPairingDenied?.();
|
|
2379
|
+
res.writeHead(204);
|
|
2380
|
+
res.end();
|
|
2381
|
+
} catch {
|
|
2382
|
+
res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
|
|
2383
|
+
res.end("invalid pairing decision");
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2353
2386
|
/**
|
|
2354
2387
|
* Tells connected extensions this shutdown is deliberate, so they drop the
|
|
2355
2388
|
* endpoint instead of retrying it for the rest of the browser session.
|
|
@@ -2365,6 +2398,13 @@ var LoopbackWSHub = class {
|
|
|
2365
2398
|
await this.close();
|
|
2366
2399
|
}
|
|
2367
2400
|
close() {
|
|
2401
|
+
if (!this.closedNotified) {
|
|
2402
|
+
this.closedNotified = true;
|
|
2403
|
+
try {
|
|
2404
|
+
this.opts.onClosed?.();
|
|
2405
|
+
} catch {
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2368
2408
|
for (const p of this.pending.values()) {
|
|
2369
2409
|
clearTimeout(p.timer);
|
|
2370
2410
|
p.reject(new Error("hub closing"));
|
|
@@ -2572,12 +2612,13 @@ function helloToWorker(h, now) {
|
|
|
2572
2612
|
last_heartbeat_at: now
|
|
2573
2613
|
};
|
|
2574
2614
|
}
|
|
2575
|
-
function installWire(hub, coordinator, clock) {
|
|
2615
|
+
function installWire(hub, coordinator, clock, onWorkerReady) {
|
|
2576
2616
|
hub.onMessage((worker_id, msg) => {
|
|
2577
2617
|
switch (msg.type) {
|
|
2578
2618
|
case "HELLO":
|
|
2579
2619
|
coordinator.unregisterWorker(msg.worker_id);
|
|
2580
2620
|
coordinator.registerWorker(helloToWorker(msg, clock.now()));
|
|
2621
|
+
onWorkerReady?.(msg.worker_id);
|
|
2581
2622
|
break;
|
|
2582
2623
|
case "PING":
|
|
2583
2624
|
coordinator.heartbeatWorker(worker_id);
|
|
@@ -2648,12 +2689,53 @@ function processInfo(cwd = process.cwd()) {
|
|
|
2648
2689
|
}
|
|
2649
2690
|
|
|
2650
2691
|
// src/ensure-hub.ts
|
|
2692
|
+
var DEFAULT_WORKER_WAIT_TIMEOUT_MS = 9e4;
|
|
2651
2693
|
function createHubEnsurer(d) {
|
|
2652
2694
|
let bundle = null;
|
|
2653
2695
|
let inflight = null;
|
|
2654
2696
|
let pairUrlValue = null;
|
|
2655
2697
|
let opened = false;
|
|
2698
|
+
let hubIsClosed = false;
|
|
2699
|
+
let pendingDenial = false;
|
|
2700
|
+
let workerWait = null;
|
|
2656
2701
|
const info = d.processInfo ?? processInfo();
|
|
2702
|
+
function connectedWorkerId(coordinator) {
|
|
2703
|
+
const workers = coordinator.workers.list();
|
|
2704
|
+
if (workers.length === 0) return null;
|
|
2705
|
+
if (workers.length > 1) {
|
|
2706
|
+
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");
|
|
2707
|
+
}
|
|
2708
|
+
return workers[0].id;
|
|
2709
|
+
}
|
|
2710
|
+
function finishWorkerWait(result) {
|
|
2711
|
+
const pending = workerWait;
|
|
2712
|
+
if (!pending) return;
|
|
2713
|
+
workerWait = null;
|
|
2714
|
+
clearTimeout(pending.timer);
|
|
2715
|
+
if ("workerId" in result) pending.resolve(result.workerId);
|
|
2716
|
+
else pending.reject(result.error);
|
|
2717
|
+
}
|
|
2718
|
+
function notifyWorkerReady(coordinator) {
|
|
2719
|
+
try {
|
|
2720
|
+
const workerId = connectedWorkerId(coordinator);
|
|
2721
|
+
if (workerId) finishWorkerWait({ workerId });
|
|
2722
|
+
} catch (error) {
|
|
2723
|
+
finishWorkerWait({ error: error instanceof Error ? error : new Error(String(error)) });
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
function pairingDenied() {
|
|
2727
|
+
const error = new Error(
|
|
2728
|
+
"\u7528\u6237\u62D2\u7EDD\u4E86\u6D4F\u89C8\u5668\u914D\u5BF9\u6388\u6743\u3002" + (pairUrlValue ? `\u914D\u5BF9\u9875\uFF1A${pairUrlValue}` : "")
|
|
2729
|
+
);
|
|
2730
|
+
if (workerWait) finishWorkerWait({ error });
|
|
2731
|
+
else pendingDenial = true;
|
|
2732
|
+
}
|
|
2733
|
+
function hubClosed() {
|
|
2734
|
+
hubIsClosed = true;
|
|
2735
|
+
if (workerWait) {
|
|
2736
|
+
finishWorkerWait({ error: new Error("\u6D4F\u89C8\u5668\u914D\u5BF9\u670D\u52A1\u5DF2\u5173\u95ED\u3002") });
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2657
2739
|
async function bind() {
|
|
2658
2740
|
const identity = loadOrCreateIdentity(d.identityDir);
|
|
2659
2741
|
const preferred = d.explicitPort ?? loadLastPort(d.identityDir) ?? 0;
|
|
@@ -2678,11 +2760,17 @@ function createHubEnsurer(d) {
|
|
|
2678
2760
|
}
|
|
2679
2761
|
saveLastPort(port, d.identityDir);
|
|
2680
2762
|
const coordinator = new Coordinator({ hub, clock: d.clock, idGen: d.idGen });
|
|
2681
|
-
installWire(hub, coordinator, d.clock);
|
|
2763
|
+
installWire(hub, coordinator, d.clock, () => notifyWorkerReady(coordinator));
|
|
2682
2764
|
pairUrlValue = `http://127.0.0.1:${port}/pair`;
|
|
2683
2765
|
return { coordinator, hub, port };
|
|
2684
2766
|
function common() {
|
|
2685
|
-
return {
|
|
2767
|
+
return {
|
|
2768
|
+
token: d.token,
|
|
2769
|
+
clock: d.clock,
|
|
2770
|
+
idGen: d.idGen,
|
|
2771
|
+
onPairingDenied: pairingDenied,
|
|
2772
|
+
onClosed: hubClosed
|
|
2773
|
+
};
|
|
2686
2774
|
}
|
|
2687
2775
|
}
|
|
2688
2776
|
async function ensure() {
|
|
@@ -2698,10 +2786,40 @@ function createHubEnsurer(d) {
|
|
|
2698
2786
|
}
|
|
2699
2787
|
return b;
|
|
2700
2788
|
}
|
|
2789
|
+
async function waitForWorker(timeoutMs = DEFAULT_WORKER_WAIT_TIMEOUT_MS) {
|
|
2790
|
+
const { coordinator } = await ensure();
|
|
2791
|
+
if (hubIsClosed) throw new Error("\u6D4F\u89C8\u5668\u914D\u5BF9\u670D\u52A1\u5DF2\u5173\u95ED\u3002");
|
|
2792
|
+
const connected = connectedWorkerId(coordinator);
|
|
2793
|
+
if (connected) return connected;
|
|
2794
|
+
if (pendingDenial) {
|
|
2795
|
+
pendingDenial = false;
|
|
2796
|
+
throw new Error(
|
|
2797
|
+
"\u7528\u6237\u62D2\u7EDD\u4E86\u6D4F\u89C8\u5668\u914D\u5BF9\u6388\u6743\u3002" + (pairUrlValue ? `\u914D\u5BF9\u9875\uFF1A${pairUrlValue}` : "")
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
if (workerWait) return workerWait.promise;
|
|
2801
|
+
let resolve;
|
|
2802
|
+
let reject;
|
|
2803
|
+
const promise = new Promise((res, rej) => {
|
|
2804
|
+
resolve = res;
|
|
2805
|
+
reject = rej;
|
|
2806
|
+
});
|
|
2807
|
+
const timer = setTimeout(() => {
|
|
2808
|
+
finishWorkerWait({
|
|
2809
|
+
error: new Error(
|
|
2810
|
+
`\u7B49\u5F85\u6D4F\u89C8\u5668\u6388\u6743\u8D85\u65F6\uFF08${Math.ceil(timeoutMs / 1e3)} \u79D2\uFF09\u3002` + (pairUrlValue ? `\u8BF7\u5728\u5DF2\u6253\u5F00\u7684\u914D\u5BF9\u9875 ${pairUrlValue} \u5B8C\u6210\u6388\u6743\u540E\u91CD\u8BD5\u3002` : "")
|
|
2811
|
+
)
|
|
2812
|
+
});
|
|
2813
|
+
}, timeoutMs);
|
|
2814
|
+
timer.unref?.();
|
|
2815
|
+
workerWait = { promise, resolve, reject, timer };
|
|
2816
|
+
return promise;
|
|
2817
|
+
}
|
|
2701
2818
|
return {
|
|
2702
2819
|
ensure,
|
|
2703
2820
|
peek: () => bundle,
|
|
2704
2821
|
pairUrl: () => pairUrlValue,
|
|
2822
|
+
waitForWorker,
|
|
2705
2823
|
bound: () => bundle != null
|
|
2706
2824
|
};
|
|
2707
2825
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@attson/atwebpilot-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.68",
|
|
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",
|