@attson/atwebpilot-mcp 0.0.76 → 0.0.78

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 CHANGED
@@ -17,11 +17,11 @@ Claude Code:
17
17
  **不需要手填端口。** 从 Plan 33 起是配对流程:
18
18
 
19
19
  1. 会话启动时**不绑任何端口** —— 不碰网页的会话零副作用。
20
- 2. AI 第一次调 `list_tabs` / `pairing_status` 时才绑一个空闲端口,尝试用系统默认浏览器打开配对页,并立即返回 `pairing_required + pair_url`,不阻塞等待。
20
+ 2. AI 第一次调 `list_tabs` / `pairing_status` 时才绑一个空闲端口,通过 MCP URL elicitation 请宿主打开配对页,并立即返回 `pairing_required + pair_url`,不阻塞等待;不支持该能力的宿主回退到系统默认浏览器。
21
21
  3. 你在浏览器里点「允许」,扩展即接入;AI 用轻量 `pairing_status` 确认连接后再读取 tab,不重复阻塞式调用。
22
22
  4. 之后本机的会话都免确认(信任是安装级的);端口能复用时连配对页都不会弹。
23
23
 
24
- 若系统未弹出浏览器,可直接打开返回结果中的 `pair_url`。授权成功只以扩展实际建立连接并发送 `HELLO` 为准;打开配对页本身不代表已连接。
24
+ 若宿主和系统都未弹出浏览器,可直接打开返回结果中的 `pair_url`。授权成功只以扩展实际建立连接并发送 `HELLO` 为准;打开配对页本身不代表已连接。
25
25
 
26
26
  多个 Codex / Claude Code 会话可以同时接入同一个浏览器,各自一条连接。可在扩展设置 →
27
27
  Coordinator 查看已接入的会话、单独断开,或撤销信任。
@@ -49,7 +49,8 @@ listen 端口的能力,所以方向只能如此。这也是配对页存在的
49
49
  ## 工具面
50
50
 
51
51
  - 控制面 5 个:`pairing_status / list_tabs / open_session / close_session / get_quota`
52
- - 发现 1 个:`browser_discoverTools` —— 已知工具时直接传 `enable: [...]`,已知分组时直接传 `enableGroups: ["export", ...]`;只有两者都未知时才不带参数读取目录。启用后会加入本进程的 `tools/list`(发送 `tools/list_changed`),并直接返回完整 schema。
52
+ - 发现 1 个:`browser_discoverTools` —— 已知工具时直接传 `enable: [...]`,已知分组时直接传 `enableGroups: ["export", ...]`,也可用 `profile: "core" | "full"` 切换整套工具面;只有这些都未知时才不带参数读取目录。选中的工具会加入本进程的 `tools/list`(发送 `tools/list_changed`),并始终返回完整 schema,包括已经启用的工具。
53
+ - 同轮执行桥 1 个:`browser_call({session_id, tool, args})` —— 某些宿主虽已刷新 `tools/list`,却不会在当前模型轮次注入新工具。AI 可在 `discoverTools` 后立刻通过该桥执行,不需要让用户回复“继续”;下一轮看到原生工具后仍优先使用原生工具。
53
54
  - 执行面在浏览器连接前不进入 `tools/list`;连接确认后加载默认 **core 31 个** `browser_*`。其余 20 个由 AI 按需 `discoverTools` 拉取,用户不用配置。
54
55
  - 不暴露 `askUser`(MCP 会话没有人在侧边栏应答)和 `attachTab` / `detachTab`(目标 tab 已由 `open_session` 绑定)。
55
56
 
@@ -65,8 +66,8 @@ hover / uploadFile / scroll / waitFor`);`browser_storage` 也不会列出,
65
66
 
66
67
  | 值 | 效果 |
67
68
  |---|---|
68
- | `core`(默认) | 连接前仅控制面;连接后 31 个核心工具 + `browser_discoverTools` |
69
- | `full` | 连接前仍仅控制面;连接后列出全部 51 个工具 |
69
+ | `core`(默认) | 连接前仅控制面、发现工具与同轮执行桥;连接后再列出 31 个核心工具 |
70
+ | `full` | 连接前同上;连接后列出全部 51 个原生工具 |
70
71
 
71
72
  无法识别的值(包括已移除的 `parity`)按 `core` 处理,并往 stderr 打一条提示。
72
73
 
package/dist/index.js CHANGED
@@ -2109,13 +2109,23 @@ function discoveryCatalog(all, advertised) {
2109
2109
 
2110
2110
  // src/discover-tool.ts
2111
2111
  var DISCOVER_GROUPS = Object.keys(DISCOVERABLE_GROUPS);
2112
+ var DISCOVER_PROFILES = ["core", "full"];
2113
+ var PROFILE_TOOLS = {
2114
+ core: generateBrowserTools("core").map((tool) => tool.name),
2115
+ full: generateBrowserTools("full").map((tool) => tool.name)
2116
+ };
2112
2117
  var DISCOVER_TOOL = {
2113
2118
  name: "browser_discoverTools",
2114
- description: "The default tool list is the core set. Enable known extras directly with enable=[names] or enableGroups=[groups]. Omit both only when you need the catalog. Groups: export, network, storage, browser-data, inspect, legacy-dom, form, tabs. Enabled tools join tools/list and the response includes their full schemas so you can call them immediately. Use this instead of rebuilding missing capabilities with runJS.",
2119
+ description: "The default tool list is the core set. Enable known extras directly with enable=[names] or enableGroups=[groups]. Use profile=core|full to return and enable a complete profile. Omit both only when you need the catalog. Groups: export, network, storage, browser-data, inspect, legacy-dom, form, tabs. The response includes full schemas even for tools already enabled. If the host has not refreshed them in this turn, call them through browser_call. Use this instead of rebuilding missing capabilities with runJS.",
2115
2120
  inputSchema: {
2116
2121
  type: "object",
2117
2122
  properties: {
2118
2123
  enable: { type: "array", items: { type: "string" }, description: "Tool names from the catalog to advertise" },
2124
+ profile: {
2125
+ type: "string",
2126
+ enum: [...DISCOVER_PROFILES],
2127
+ description: "Return and enable every runnable tool in the core or full profile"
2128
+ },
2119
2129
  enableGroups: {
2120
2130
  type: "array",
2121
2131
  items: { type: "string", enum: DISCOVER_GROUPS },
@@ -2131,14 +2141,16 @@ function isRunnable(t, supported) {
2131
2141
  function handleDiscover(input) {
2132
2142
  const { all, advertised, args, supported } = input;
2133
2143
  const byName = new Map(all.map((t) => [t.name, t]));
2134
- const hasSelection = Array.isArray(args.enable) || Array.isArray(args.enableGroups);
2144
+ const hasSelection = Array.isArray(args.enable) || Array.isArray(args.enableGroups) || args.profile != null;
2135
2145
  if (!hasSelection) {
2136
2146
  const runnable = all.filter((t) => isRunnable(t, supported));
2137
2147
  return { catalog: discoveryCatalog(runnable, advertised), changed: false };
2138
2148
  }
2139
2149
  const explicit = Array.isArray(args.enable) ? args.enable.map(String) : [];
2140
2150
  const groups = Array.isArray(args.enableGroups) ? args.enableGroups.map(String) : [];
2151
+ const profile = args.profile == null ? null : String(args.profile);
2141
2152
  const unknownGroups = groups.filter((group) => !DISCOVER_GROUPS.includes(group));
2153
+ const unknownProfiles = profile != null && !DISCOVER_PROFILES.includes(profile) ? [profile] : [];
2142
2154
  const grouped = groups.flatMap(
2143
2155
  (group) => DISCOVERABLE_GROUPS[group] ?? []
2144
2156
  );
@@ -2150,7 +2162,13 @@ function handleDiscover(input) {
2150
2162
  for (const name of grouped) {
2151
2163
  if (!requestedByName.has(name)) requestedByName.set(name, name);
2152
2164
  }
2165
+ if (profile && unknownProfiles.length === 0) {
2166
+ for (const name of PROFILE_TOOLS[profile]) {
2167
+ if (!requestedByName.has(name)) requestedByName.set(name, name);
2168
+ }
2169
+ }
2153
2170
  const enabled = [];
2171
+ let changed = false;
2154
2172
  const unknown = [];
2155
2173
  const unsupported = [];
2156
2174
  for (const [name, reportAs] of requestedByName) {
@@ -2163,19 +2181,42 @@ function handleDiscover(input) {
2163
2181
  unsupported.push(reportAs);
2164
2182
  continue;
2165
2183
  }
2166
- if (advertised.has(name)) continue;
2167
- advertised.add(name);
2184
+ if (!advertised.has(name)) {
2185
+ advertised.add(name);
2186
+ changed = true;
2187
+ }
2168
2188
  enabled.push({ name: t.name, description: t.description, inputSchema: t.inputSchema });
2169
2189
  }
2170
2190
  return {
2171
2191
  enabled,
2172
2192
  ...unknown.length ? { unknown } : {},
2173
2193
  ...unknownGroups.length ? { unknownGroups } : {},
2194
+ ...unknownProfiles.length ? { unknownProfiles } : {},
2174
2195
  ...unsupported.length ? { unsupported } : {},
2175
- changed: enabled.length > 0
2196
+ changed
2176
2197
  };
2177
2198
  }
2178
2199
 
2200
+ // src/browser-call.ts
2201
+ var BROWSER_CALL_TOOL = {
2202
+ name: "browser_call",
2203
+ description: "Call an enabled browser tool in the current turn after browser_discoverTools returns its schema. Prefer the native tool when available.",
2204
+ inputSchema: {
2205
+ type: "object",
2206
+ required: ["session_id", "tool", "args"],
2207
+ properties: {
2208
+ session_id: { type: "string", description: "Session id from open_session" },
2209
+ tool: { type: "string", description: "Enabled browser tool name; browser_ prefix is optional" },
2210
+ args: {
2211
+ type: "object",
2212
+ description: "Arguments matching the schema returned by browser_discoverTools, excluding session_id",
2213
+ additionalProperties: true
2214
+ }
2215
+ },
2216
+ additionalProperties: false
2217
+ }
2218
+ };
2219
+
2179
2220
  // ../shared/src/static-scan.ts
2180
2221
  var RULES = [
2181
2222
  {
@@ -2423,7 +2464,7 @@ var TOPICS = {
2423
2464
  2. After approval, call pairing_status. Do not retry list_tabs while status is unchanged.
2424
2465
  3. Call list_tabs when connected, choose a tab, then open_session with its tab_id.
2425
2466
  4. For interaction use browser_getPageInfo then browser_takeSnapshot; for broad reading use browser_createPageIndex.
2426
- 5. Prefer purpose-built tools and verified results. Use browser_discoverTools only for capabilities outside the connected core surface.
2467
+ 5. 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".
2427
2468
  6. Close the session when finished.
2428
2469
  `,
2429
2470
  pairing: `# AtWebPilot pairing
@@ -2502,7 +2543,7 @@ var SKILL_TOOL = {
2502
2543
  // package.json
2503
2544
  var package_default = {
2504
2545
  name: "@attson/atwebpilot-mcp",
2505
- version: "0.0.76",
2546
+ version: "0.0.78",
2506
2547
  license: "Apache-2.0",
2507
2548
  description: "MCP server for the atwebpilot browser extension - let Codex or Claude Code drive your browser.",
2508
2549
  homepage: "https://github.com/attson/atwebpilot#readme",
@@ -2552,6 +2593,45 @@ var package_default = {
2552
2593
  // src/version.ts
2553
2594
  var MCP_VERSION = package_default.version;
2554
2595
 
2596
+ // src/pairing-presenter.ts
2597
+ import { randomUUID } from "crypto";
2598
+ function createPairingPresenter(server, fallbackOpenUrl) {
2599
+ let presentedUrl = null;
2600
+ const runFallback = async (url) => {
2601
+ if (!fallbackOpenUrl) return;
2602
+ try {
2603
+ await fallbackOpenUrl(url);
2604
+ } catch (error) {
2605
+ presentedUrl = null;
2606
+ console.error(
2607
+ `[atwebpilot-mcp] failed to open pairing page ${url}:`,
2608
+ error instanceof Error ? error.message : String(error)
2609
+ );
2610
+ }
2611
+ };
2612
+ return (url) => {
2613
+ if (presentedUrl === url) return;
2614
+ presentedUrl = url;
2615
+ if (!server.getClientCapabilities()?.elicitation?.url) {
2616
+ void runFallback(url);
2617
+ return;
2618
+ }
2619
+ void server.elicitInput({
2620
+ mode: "url",
2621
+ message: "Open AtWebPilot in the browser to approve this pairing request.",
2622
+ url,
2623
+ elicitationId: `atwebpilot-pairing-${randomUUID()}`
2624
+ }).catch((error) => {
2625
+ if (!server.transport) return;
2626
+ console.error(
2627
+ "[atwebpilot-mcp] URL elicitation failed; falling back to the system browser:",
2628
+ error instanceof Error ? error.message : String(error)
2629
+ );
2630
+ return runFallback(url);
2631
+ });
2632
+ };
2633
+ }
2634
+
2555
2635
  // src/mcp-server.ts
2556
2636
  var ALL_BROWSER_TOOLS = generateBrowserTools("full");
2557
2637
  var BROWSER_BY_NAME = new Map(ALL_BROWSER_TOOLS.map((t) => [t.name, t]));
@@ -2598,6 +2678,7 @@ function buildToolList(deps, state = DEFAULT_STATE) {
2598
2678
  { name: SKILL_TOOL.name, description: SKILL_TOOL.description, inputSchema: SKILL_TOOL.inputSchema },
2599
2679
  ...CONTROL_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
2600
2680
  { name: DISCOVER_TOOL.name, description: DISCOVER_TOOL.description, inputSchema: DISCOVER_TOOL.inputSchema },
2681
+ { name: BROWSER_CALL_TOOL.name, description: BROWSER_CALL_TOOL.description, inputSchema: BROWSER_CALL_TOOL.inputSchema },
2601
2682
  ...browser.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
2602
2683
  ];
2603
2684
  }
@@ -2617,7 +2698,7 @@ function toolResult(gen, data) {
2617
2698
  return { content };
2618
2699
  }
2619
2700
  var fail2 = (message) => ({ content: [{ type: "text", text: message }], isError: true });
2620
- async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChanged) {
2701
+ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChanged, onPairingRequired) {
2621
2702
  try {
2622
2703
  if (name === SKILL_TOOL.name) {
2623
2704
  const bundle = readSkillBundle(args.topic);
@@ -2634,14 +2715,19 @@ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChang
2634
2715
  const { changed: _c, ...body } = r;
2635
2716
  return ok(body);
2636
2717
  }
2718
+ if (name === BROWSER_CALL_TOOL.name) {
2719
+ return handleBrowserCall(deps, args, state);
2720
+ }
2637
2721
  if (name === "pairing_status") {
2638
2722
  const result = await handlePairingStatus(deps);
2639
2723
  if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
2724
+ else onPairingRequired?.(result.pair_url);
2640
2725
  return ok(result);
2641
2726
  }
2642
2727
  if (name === "list_tabs") {
2643
2728
  const result = await handleListTabs(deps);
2644
2729
  if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
2730
+ else onPairingRequired?.(result.pair_url);
2645
2731
  return ok(result);
2646
2732
  }
2647
2733
  if (name === "open_session") {
@@ -2655,19 +2741,50 @@ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChang
2655
2741
  if (gen) return toolResult(gen, await handleBrowserTool(deps, gen, args));
2656
2742
  return fail2(`unknown tool: ${name}`);
2657
2743
  } catch (e) {
2658
- return fail2(e instanceof Error ? e.message : String(e));
2744
+ const message = e instanceof Error ? e.message : String(e);
2745
+ if (message.includes("PAIRING_REQUIRED")) {
2746
+ const pairUrl = deps.pairUrl();
2747
+ if (pairUrl) onPairingRequired?.(pairUrl);
2748
+ }
2749
+ return fail2(message);
2750
+ }
2751
+ }
2752
+ async function handleBrowserCall(deps, args, state) {
2753
+ const rawName = typeof args.tool === "string" ? args.tool.trim() : "";
2754
+ if (!rawName) return fail2("InvalidArgs: browser_call requires tool");
2755
+ const name = rawName.startsWith("browser_") ? rawName : `browser_${rawName}`;
2756
+ const gen = BROWSER_BY_NAME.get(name);
2757
+ if (!gen) return fail2(`Unknown browser tool: ${rawName}`);
2758
+ if (!state.advertised.has(name)) {
2759
+ return fail2(`${name} is not enabled; call browser_discoverTools first`);
2659
2760
  }
2761
+ const supported = workerToolSupport(deps);
2762
+ if (supported && !gen.builtinTools.every((tool) => supported.has(tool))) {
2763
+ return fail2(`${name} is not supported by the connected browser extension`);
2764
+ }
2765
+ if (typeof args.session_id !== "string" || args.session_id.length === 0) {
2766
+ return fail2("InvalidArgs: browser_call requires session_id");
2767
+ }
2768
+ if (args.args === null || typeof args.args !== "object" || Array.isArray(args.args)) {
2769
+ return fail2("InvalidArgs: browser_call args must be an object");
2770
+ }
2771
+ const toolArgs = {
2772
+ ...args.args,
2773
+ session_id: args.session_id
2774
+ };
2775
+ return toolResult(gen, await handleBrowserTool(deps, gen, toolArgs));
2660
2776
  }
2661
2777
  async function revealBrowserSurface(state, onListChanged) {
2662
2778
  if (state.browserSurfaceVisible) return;
2663
2779
  state.browserSurfaceVisible = true;
2664
2780
  await onListChanged?.();
2665
2781
  }
2666
- function createMcpServer(deps, state = DEFAULT_STATE) {
2782
+ function createMcpServer(deps, state = DEFAULT_STATE, options = {}) {
2667
2783
  const server = new Server(
2668
2784
  { name: "atwebpilot-mcp", version: MCP_VERSION },
2669
2785
  { capabilities: { tools: { listChanged: true }, logging: {} } }
2670
2786
  );
2787
+ const presentPairing = createPairingPresenter(server, options.fallbackOpenUrl);
2671
2788
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList(deps, state) }));
2672
2789
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
2673
2790
  const args = req.params.arguments ?? {};
@@ -2678,7 +2795,7 @@ function createMcpServer(deps, state = DEFAULT_STATE) {
2678
2795
  console.error("[atwebpilot-mcp] failed to send tools/list_changed:", error instanceof Error ? error.message : String(error));
2679
2796
  }
2680
2797
  };
2681
- return dispatchCall(deps, req.params.name, args, state, onListChanged);
2798
+ return dispatchCall(deps, req.params.name, args, state, onListChanged, presentPairing);
2682
2799
  });
2683
2800
  return server;
2684
2801
  }
@@ -3119,7 +3236,7 @@ function installWire(hub, coordinator, clock, onWorkerReady) {
3119
3236
  import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
3120
3237
  import { homedir } from "os";
3121
3238
  import { join as join2 } from "path";
3122
- import { randomBytes, randomUUID } from "crypto";
3239
+ import { randomBytes, randomUUID as randomUUID2 } from "crypto";
3123
3240
  var FILE = "identity.json";
3124
3241
  function defaultIdentityDir() {
3125
3242
  return join2(homedir(), ".atwebpilot");
@@ -3143,7 +3260,7 @@ function loadOrCreateIdentity(dir = defaultIdentityDir()) {
3143
3260
  const existing = read(dir);
3144
3261
  if (existing) return { installId: existing.installId, secret: existing.secret };
3145
3262
  const created = {
3146
- installId: `inst_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
3263
+ installId: `inst_${randomUUID2().replace(/-/g, "").slice(0, 16)}`,
3147
3264
  secret: randomBytes(24).toString("base64url")
3148
3265
  };
3149
3266
  write(dir, created);
@@ -3162,7 +3279,7 @@ function processInfo(cwd = process.cwd()) {
3162
3279
  const home = homedir();
3163
3280
  const label = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
3164
3281
  return {
3165
- sessionId: `sess_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
3282
+ sessionId: `sess_${randomUUID2().replace(/-/g, "").slice(0, 12)}`,
3166
3283
  label,
3167
3284
  pid: process.pid
3168
3285
  };
@@ -3387,10 +3504,9 @@ async function main() {
3387
3504
  clock,
3388
3505
  idGen,
3389
3506
  explicitPort,
3390
- token,
3391
- openUrl: defaultOpenUrl
3507
+ token
3392
3508
  });
3393
- const server = createMcpServer(deps);
3509
+ const server = createMcpServer(deps, void 0, { fallbackOpenUrl: defaultOpenUrl });
3394
3510
  await server.connect(new StdioServerTransport());
3395
3511
  console.error("[atwebpilot-mcp] stdio MCP connected; ws port binds on first browser use");
3396
3512
  const shutdown = () => {
@@ -64,16 +64,26 @@ do this, in order:
64
64
  `browser_discoverTools({ enable: ["browser_downloadSpreadsheet", ...] })`.
65
65
  If you know only the group, use
66
66
  `browser_discoverTools({ enableGroups: ["export"] })`.
67
+ To switch the whole process surface explicitly, use
68
+ `browser_discoverTools({ profile: "core" })` or `{ profile: "full" }`.
67
69
  2. Call `browser_discoverTools({})` only when neither the tool nor group is
68
70
  known. Enabled tools join `tools/list` (a `tools/list_changed` notification
69
- is sent), and the response carries their full schemas for immediate use.
71
+ is sent), and the response carries the selected full schemas even if those
72
+ tools were already enabled.
73
+ 3. Prefer a newly visible native tool. If the MCP host has refreshed
74
+ `tools/list` but the current model turn still has the old snapshot, call the
75
+ selected tool immediately through
76
+ `browser_call({ session_id, tool: "browser_downloadSpreadsheet", args: {...} })`.
77
+ Do not ask the user to reply "continue" just to start another model turn.
78
+ Nested `args` omit `session_id`; the bridge injects the top-level value.
70
79
 
71
80
  **Do not** rebuild these capabilities with `runJS` (`fetch`, CSV blobs,
72
81
  `localStorage[...]`). It is slower, loses schema validation, and usually trips
73
82
  the dangerous-tier review that the purpose-built tool would have avoided.
74
83
 
75
84
  `ATWEBPILOT_MCP_TOOLS=full` advertises everything from the start for users who
76
- prefer to pay the context cost once.
85
+ prefer to pay the context cost once. `browser_call` is a current-turn
86
+ compatibility path; use the native tool once the host exposes it.
77
87
 
78
88
  ## Recommended flow
79
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attson/atwebpilot-mcp",
3
- "version": "0.0.76",
3
+ "version": "0.0.78",
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",