@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 +6 -5
- package/dist/index.js +214 -20
- package/dist/skill/SKILL.md +12 -2
- 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` / `pairing_status`
|
|
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
|
-
|
|
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", ...]
|
|
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`(默认) |
|
|
69
|
-
| `full` |
|
|
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.
|
|
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))
|
|
2167
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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_${
|
|
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_${
|
|
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, {
|
|
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 = () => {
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|