@attson/atwebpilot-mcp 0.0.67 → 0.0.69

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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +135 -17
  3. 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
@@ -1312,7 +1312,7 @@ var TOOL_DEFS = [
1312
1312
  },
1313
1313
  {
1314
1314
  name: "runJS",
1315
- description: "[LAST RESORT\xB7DANGER] \u5728 MAIN world \u6CE8\u5165\u5E76\u6267\u884C async \u51FD\u6570\u4F53\uFF08receives `ctx` = bindings\uFF09\u3002\u5FC5\u987B return \u503C\u3002\n**\u4EC5\u5728\u7ED3\u6784\u5316\u5DE5\u5177\u4E0D\u591F\u7528\u65F6\u4F7F\u7528**\u2014\u2014\u4F1A\u7ECF\u8FC7\u9759\u6001\u626B\u63CF\u4E0E\u4EBA\u5DE5\u5BA1\u9605\u3002",
1315
+ description: "[LAST RESORT\xB7DANGER] \u5728 MAIN world \u6CE8\u5165\u5E76\u6267\u884C async \u51FD\u6570\u4F53\uFF08receives `ctx` = bindings\uFF09\u3002\u5FC5\u987B return \u503C\u3002\n**\u4EC5\u5728\u7ED3\u6784\u5316\u5DE5\u5177\u4E0D\u591F\u7528\u65F6\u4F7F\u7528**\u2014\u2014\u4F1A\u7ECF\u8FC7\u9759\u6001\u626B\u63CF\u4E0E\u4EBA\u5DE5\u5BA1\u9605\u3002\u4E25\u683C CSP \u9875\u9762\u9700\u8981\u7528\u6237\u9884\u5148\u5728\u6269\u5C55\u8BBE\u7F6E\u4E2D\u5F00\u542F CDP/debugger\uFF1B\u8BFB\u53D6\u6837\u5F0F\u3001\u4F4D\u7F6E\u6216\u7956\u5148\u94FE\u8BF7\u4F18\u5148\u4F7F\u7528 inspectElement\u3002",
1316
1316
  input_schema: {
1317
1317
  type: "object",
1318
1318
  properties: {
@@ -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 w = coordinator.workers.get(singleWorkerId(coordinator, deps.pairUrl()));
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 = singleWorkerId(coordinator, deps.pairUrl());
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 { createServer } from "http";
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 { token: d.token, clock: d.clock, idGen: d.idGen };
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.67",
3
+ "version": "0.0.69",
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",