@attson/atwebpilot-mcp 0.0.77 → 0.0.79

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.77",
2546
+ version: "0.0.79",
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,64 @@ 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 isCodexClient(server) {
2599
+ return server.getClientVersion?.()?.name.toLowerCase().includes("codex") ?? false;
2600
+ }
2601
+ function createPairingPresenter(server, fallbackOpenUrl) {
2602
+ let presentedUrl = null;
2603
+ const runFallback = async (url) => {
2604
+ if (!fallbackOpenUrl) return false;
2605
+ try {
2606
+ await fallbackOpenUrl(url);
2607
+ return true;
2608
+ } catch (error) {
2609
+ console.error(
2610
+ `[atwebpilot-mcp] failed to open pairing page ${url}:`,
2611
+ error instanceof Error ? error.message : String(error)
2612
+ );
2613
+ return false;
2614
+ }
2615
+ };
2616
+ const requestHostPresentation = (url, fallbackOnFailure) => {
2617
+ void server.elicitInput({
2618
+ mode: "url",
2619
+ message: "Open AtWebPilot in the browser to approve this pairing request.",
2620
+ url,
2621
+ elicitationId: `atwebpilot-pairing-${randomUUID()}`
2622
+ }).catch(async (error) => {
2623
+ if (!server.transport) return;
2624
+ console.error(
2625
+ fallbackOnFailure ? "[atwebpilot-mcp] URL elicitation failed; falling back to the system browser:" : "[atwebpilot-mcp] URL elicitation failed:",
2626
+ error instanceof Error ? error.message : String(error)
2627
+ );
2628
+ if (fallbackOnFailure && await runFallback(url)) return;
2629
+ presentedUrl = null;
2630
+ });
2631
+ };
2632
+ return (url) => {
2633
+ if (presentedUrl === url) return;
2634
+ presentedUrl = url;
2635
+ const supportsUrlElicitation = Boolean(server.getClientCapabilities()?.elicitation?.url);
2636
+ if (isCodexClient(server)) {
2637
+ void runFallback(url).then((opened) => {
2638
+ if (opened || !server.transport) return;
2639
+ if (supportsUrlElicitation) requestHostPresentation(url, false);
2640
+ else presentedUrl = null;
2641
+ });
2642
+ return;
2643
+ }
2644
+ if (supportsUrlElicitation) {
2645
+ requestHostPresentation(url, true);
2646
+ return;
2647
+ }
2648
+ void runFallback(url).then((opened) => {
2649
+ if (!opened) presentedUrl = null;
2650
+ });
2651
+ };
2652
+ }
2653
+
2555
2654
  // src/mcp-server.ts
2556
2655
  var ALL_BROWSER_TOOLS = generateBrowserTools("full");
2557
2656
  var BROWSER_BY_NAME = new Map(ALL_BROWSER_TOOLS.map((t) => [t.name, t]));
@@ -2598,6 +2697,7 @@ function buildToolList(deps, state = DEFAULT_STATE) {
2598
2697
  { name: SKILL_TOOL.name, description: SKILL_TOOL.description, inputSchema: SKILL_TOOL.inputSchema },
2599
2698
  ...CONTROL_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
2600
2699
  { name: DISCOVER_TOOL.name, description: DISCOVER_TOOL.description, inputSchema: DISCOVER_TOOL.inputSchema },
2700
+ { name: BROWSER_CALL_TOOL.name, description: BROWSER_CALL_TOOL.description, inputSchema: BROWSER_CALL_TOOL.inputSchema },
2601
2701
  ...browser.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
2602
2702
  ];
2603
2703
  }
@@ -2617,7 +2717,7 @@ function toolResult(gen, data) {
2617
2717
  return { content };
2618
2718
  }
2619
2719
  var fail2 = (message) => ({ content: [{ type: "text", text: message }], isError: true });
2620
- async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChanged) {
2720
+ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChanged, onPairingRequired) {
2621
2721
  try {
2622
2722
  if (name === SKILL_TOOL.name) {
2623
2723
  const bundle = readSkillBundle(args.topic);
@@ -2634,14 +2734,19 @@ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChang
2634
2734
  const { changed: _c, ...body } = r;
2635
2735
  return ok(body);
2636
2736
  }
2737
+ if (name === BROWSER_CALL_TOOL.name) {
2738
+ return handleBrowserCall(deps, args, state);
2739
+ }
2637
2740
  if (name === "pairing_status") {
2638
2741
  const result = await handlePairingStatus(deps);
2639
2742
  if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
2743
+ else onPairingRequired?.(result.pair_url);
2640
2744
  return ok(result);
2641
2745
  }
2642
2746
  if (name === "list_tabs") {
2643
2747
  const result = await handleListTabs(deps);
2644
2748
  if (result.status === "connected") await revealBrowserSurface(state, onListChanged);
2749
+ else onPairingRequired?.(result.pair_url);
2645
2750
  return ok(result);
2646
2751
  }
2647
2752
  if (name === "open_session") {
@@ -2655,19 +2760,50 @@ async function dispatchCall(deps, name, args, state = DEFAULT_STATE, onListChang
2655
2760
  if (gen) return toolResult(gen, await handleBrowserTool(deps, gen, args));
2656
2761
  return fail2(`unknown tool: ${name}`);
2657
2762
  } catch (e) {
2658
- return fail2(e instanceof Error ? e.message : String(e));
2763
+ const message = e instanceof Error ? e.message : String(e);
2764
+ if (message.includes("PAIRING_REQUIRED")) {
2765
+ const pairUrl = deps.pairUrl();
2766
+ if (pairUrl) onPairingRequired?.(pairUrl);
2767
+ }
2768
+ return fail2(message);
2769
+ }
2770
+ }
2771
+ async function handleBrowserCall(deps, args, state) {
2772
+ const rawName = typeof args.tool === "string" ? args.tool.trim() : "";
2773
+ if (!rawName) return fail2("InvalidArgs: browser_call requires tool");
2774
+ const name = rawName.startsWith("browser_") ? rawName : `browser_${rawName}`;
2775
+ const gen = BROWSER_BY_NAME.get(name);
2776
+ if (!gen) return fail2(`Unknown browser tool: ${rawName}`);
2777
+ if (!state.advertised.has(name)) {
2778
+ return fail2(`${name} is not enabled; call browser_discoverTools first`);
2779
+ }
2780
+ const supported = workerToolSupport(deps);
2781
+ if (supported && !gen.builtinTools.every((tool) => supported.has(tool))) {
2782
+ return fail2(`${name} is not supported by the connected browser extension`);
2783
+ }
2784
+ if (typeof args.session_id !== "string" || args.session_id.length === 0) {
2785
+ return fail2("InvalidArgs: browser_call requires session_id");
2659
2786
  }
2787
+ if (args.args === null || typeof args.args !== "object" || Array.isArray(args.args)) {
2788
+ return fail2("InvalidArgs: browser_call args must be an object");
2789
+ }
2790
+ const toolArgs = {
2791
+ ...args.args,
2792
+ session_id: args.session_id
2793
+ };
2794
+ return toolResult(gen, await handleBrowserTool(deps, gen, toolArgs));
2660
2795
  }
2661
2796
  async function revealBrowserSurface(state, onListChanged) {
2662
2797
  if (state.browserSurfaceVisible) return;
2663
2798
  state.browserSurfaceVisible = true;
2664
2799
  await onListChanged?.();
2665
2800
  }
2666
- function createMcpServer(deps, state = DEFAULT_STATE) {
2801
+ function createMcpServer(deps, state = DEFAULT_STATE, options = {}) {
2667
2802
  const server = new Server(
2668
2803
  { name: "atwebpilot-mcp", version: MCP_VERSION },
2669
2804
  { capabilities: { tools: { listChanged: true }, logging: {} } }
2670
2805
  );
2806
+ const presentPairing = createPairingPresenter(server, options.fallbackOpenUrl);
2671
2807
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList(deps, state) }));
2672
2808
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
2673
2809
  const args = req.params.arguments ?? {};
@@ -2678,11 +2814,14 @@ function createMcpServer(deps, state = DEFAULT_STATE) {
2678
2814
  console.error("[atwebpilot-mcp] failed to send tools/list_changed:", error instanceof Error ? error.message : String(error));
2679
2815
  }
2680
2816
  };
2681
- return dispatchCall(deps, req.params.name, args, state, onListChanged);
2817
+ return dispatchCall(deps, req.params.name, args, state, onListChanged, presentPairing);
2682
2818
  });
2683
2819
  return server;
2684
2820
  }
2685
2821
 
2822
+ // src/ensure-hub.ts
2823
+ import { readFile } from "fs/promises";
2824
+
2686
2825
  // src/loopback-ws-hub.ts
2687
2826
  import { WebSocketServer, WebSocket } from "ws";
2688
2827
  import {
@@ -3119,7 +3258,7 @@ function installWire(hub, coordinator, clock, onWorkerReady) {
3119
3258
  import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
3120
3259
  import { homedir } from "os";
3121
3260
  import { join as join2 } from "path";
3122
- import { randomBytes, randomUUID } from "crypto";
3261
+ import { randomBytes, randomUUID as randomUUID2 } from "crypto";
3123
3262
  var FILE = "identity.json";
3124
3263
  function defaultIdentityDir() {
3125
3264
  return join2(homedir(), ".atwebpilot");
@@ -3143,7 +3282,7 @@ function loadOrCreateIdentity(dir = defaultIdentityDir()) {
3143
3282
  const existing = read(dir);
3144
3283
  if (existing) return { installId: existing.installId, secret: existing.secret };
3145
3284
  const created = {
3146
- installId: `inst_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
3285
+ installId: `inst_${randomUUID2().replace(/-/g, "").slice(0, 16)}`,
3147
3286
  secret: randomBytes(24).toString("base64url")
3148
3287
  };
3149
3288
  write(dir, created);
@@ -3162,7 +3301,7 @@ function processInfo(cwd = process.cwd()) {
3162
3301
  const home = homedir();
3163
3302
  const label = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
3164
3303
  return {
3165
- sessionId: `sess_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
3304
+ sessionId: `sess_${randomUUID2().replace(/-/g, "").slice(0, 12)}`,
3166
3305
  label,
3167
3306
  pid: process.pid
3168
3307
  };
@@ -3327,6 +3466,57 @@ function createHubEnsurer(d) {
3327
3466
  bound: () => bundle != null
3328
3467
  };
3329
3468
  }
3469
+ var DESKTOP_ENV_KEYS = [
3470
+ "DISPLAY",
3471
+ "WAYLAND_DISPLAY",
3472
+ "XDG_RUNTIME_DIR",
3473
+ "DBUS_SESSION_BUS_ADDRESS",
3474
+ "XAUTHORITY"
3475
+ ];
3476
+ function processId(field, status) {
3477
+ const match = new RegExp(`^${field}:\\s+(\\d+)`, "m").exec(status);
3478
+ if (!match) return null;
3479
+ const value = Number(match[1]);
3480
+ return Number.isSafeInteger(value) ? value : null;
3481
+ }
3482
+ function copyDesktopVariables(target, serialized) {
3483
+ for (const entry of serialized.split("\0")) {
3484
+ const separator = entry.indexOf("=");
3485
+ if (separator <= 0) continue;
3486
+ const key = entry.slice(0, separator);
3487
+ if (!DESKTOP_ENV_KEYS.includes(key) || target[key]) continue;
3488
+ const value = entry.slice(separator + 1);
3489
+ if (value) target[key] = value;
3490
+ }
3491
+ }
3492
+ async function resolveDesktopOpenEnvironment(options = {}) {
3493
+ const environment = { ...options.baseEnv ?? process.env };
3494
+ if ((options.platform ?? process.platform) !== "linux") return environment;
3495
+ const expectedUid = options.uid ?? process.getuid?.();
3496
+ const readTextFile = options.readTextFile ?? ((path) => readFile(path, "utf8"));
3497
+ const seen = /* @__PURE__ */ new Set();
3498
+ let pid = options.parentPid ?? process.ppid;
3499
+ const maxDepth = options.maxDepth ?? 12;
3500
+ for (let depth = 0; depth < maxDepth && pid > 1 && !seen.has(pid); depth += 1) {
3501
+ seen.add(pid);
3502
+ let status;
3503
+ try {
3504
+ status = await readTextFile(`/proc/${pid}/status`);
3505
+ } catch {
3506
+ break;
3507
+ }
3508
+ const ownerUid = processId("Uid", status);
3509
+ if (expectedUid !== void 0 && ownerUid !== expectedUid) break;
3510
+ try {
3511
+ copyDesktopVariables(environment, await readTextFile(`/proc/${pid}/environ`));
3512
+ } catch {
3513
+ }
3514
+ const parentPid = processId("PPid", status);
3515
+ if (parentPid === null || parentPid === pid) break;
3516
+ pid = parentPid;
3517
+ }
3518
+ return environment;
3519
+ }
3330
3520
  function openCommands(url) {
3331
3521
  if (process.platform === "darwin") return [{ command: "open", args: [url] }];
3332
3522
  if (process.platform === "win32") {
@@ -3340,10 +3530,14 @@ function openCommands(url) {
3340
3530
  { command: "gio", args: ["open", url] }
3341
3531
  ];
3342
3532
  }
3343
- async function runOpenCommand({ command, args }) {
3533
+ async function runOpenCommand({ command, args }, environment) {
3344
3534
  const { spawn } = await import("child_process");
3345
3535
  await new Promise((resolve, reject) => {
3346
- const child = spawn(command, args, { stdio: "ignore", detached: true });
3536
+ const child = spawn(command, args, {
3537
+ stdio: "ignore",
3538
+ detached: true,
3539
+ env: environment
3540
+ });
3347
3541
  let settled = false;
3348
3542
  const finish = (error) => {
3349
3543
  if (settled) return;
@@ -3366,9 +3560,10 @@ async function runOpenCommand({ command, args }) {
3366
3560
  }
3367
3561
  async function defaultOpenUrl(url) {
3368
3562
  const errors = [];
3563
+ const environment = await resolveDesktopOpenEnvironment();
3369
3564
  for (const candidate of openCommands(url)) {
3370
3565
  try {
3371
- await runOpenCommand(candidate);
3566
+ await runOpenCommand(candidate, environment);
3372
3567
  return;
3373
3568
  } catch (error) {
3374
3569
  errors.push(error instanceof Error ? error.message : String(error));
@@ -3387,10 +3582,9 @@ async function main() {
3387
3582
  clock,
3388
3583
  idGen,
3389
3584
  explicitPort,
3390
- token,
3391
- openUrl: defaultOpenUrl
3585
+ token
3392
3586
  });
3393
- const server = createMcpServer(deps);
3587
+ const server = createMcpServer(deps, void 0, { fallbackOpenUrl: defaultOpenUrl });
3394
3588
  await server.connect(new StdioServerTransport());
3395
3589
  console.error("[atwebpilot-mcp] stdio MCP connected; ws port binds on first browser use");
3396
3590
  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.77",
3
+ "version": "0.0.79",
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",