@tekmidian/pai 0.40.0 → 0.41.0

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
@@ -112,6 +112,7 @@ pai worker replay <id> # transcript of a finished or run
112
112
  pai worker say <id> <text> # message a running worker mid-run
113
113
  pai worker handoff '<json>' # from inside a worker: report to the parent
114
114
  pai worker merge <id> # merge the worker's branch back, drop the worktree
115
+ pai worker wait <id>... # block until workers finish (never sleep-loop)
115
116
  pai worker watch # ps refreshed every 2 seconds
116
117
  ```
117
118
 
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ import { i as number, o as string, r as boolean } from "../schemas-De1F-ktH.mjs";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ //#region src/browser-mcp/bridge-client.ts
7
+ /**
8
+ * WebSocket client for the browser bridge.
9
+ *
10
+ * Talks to the native host's hand-rolled WS server (ws://127.0.0.1:8756) with
11
+ * the global WebSocket — node >= 22, no external dependency. Requests carry an
12
+ * id; replies are correlated by it. The socket is connected lazily per call
13
+ * and reconnected on demand, so the server survives Chrome being closed.
14
+ */
15
+ const BRIDGE_PORT = Number(process.env.PAI_BROWSER_BRIDGE_PORT || 8756);
16
+ const BRIDGE_URL = `ws://127.0.0.1:${BRIDGE_PORT}`;
17
+ const NOT_CONNECTED_MESSAGE = "Chrome bridge not connected — start Chrome (the PAI browser-bridge extension must be running) and try again.";
18
+ const globalWebSocketFactory = (url) => {
19
+ const WS = globalThis.WebSocket;
20
+ if (!WS) throw new Error("this node runtime has no global WebSocket — pai-browser-mcp needs node >= 22");
21
+ return new WS(url);
22
+ };
23
+ var NotConnectedError = class extends Error {
24
+ constructor() {
25
+ super(NOT_CONNECTED_MESSAGE);
26
+ this.name = "NotConnectedError";
27
+ }
28
+ };
29
+ const CONNECT_TIMEOUT_MS = 5e3;
30
+ const REQUEST_TIMEOUT_MS = 5e3;
31
+ const OPEN = 1;
32
+ var BridgeClient = class {
33
+ socket = null;
34
+ connecting = null;
35
+ nextId = 1;
36
+ pending = /* @__PURE__ */ new Map();
37
+ constructor(url = BRIDGE_URL, socketFactory = globalWebSocketFactory) {
38
+ this.url = url;
39
+ this.socketFactory = socketFactory;
40
+ }
41
+ /** True when the underlying socket is open. */
42
+ get connected() {
43
+ return this.socket !== null && this.socket.readyState === OPEN;
44
+ }
45
+ ensureSocket() {
46
+ if (this.socket && this.socket.readyState === OPEN) return Promise.resolve(this.socket);
47
+ if (this.connecting) return this.connecting;
48
+ this.dropSocket();
49
+ const attempt = new Promise((resolve, reject) => {
50
+ let socket;
51
+ try {
52
+ socket = this.socketFactory(this.url);
53
+ } catch (e) {
54
+ reject(e);
55
+ return;
56
+ }
57
+ const timer = setTimeout(() => {
58
+ reject(new NotConnectedError());
59
+ socket.onopen = null;
60
+ socket.onclose = null;
61
+ socket.onerror = null;
62
+ socket.onmessage = null;
63
+ try {
64
+ socket.close();
65
+ } catch {}
66
+ }, CONNECT_TIMEOUT_MS);
67
+ socket.onopen = () => {
68
+ clearTimeout(timer);
69
+ this.socket = socket;
70
+ resolve(socket);
71
+ };
72
+ socket.onclose = () => {
73
+ clearTimeout(timer);
74
+ this.dropSocket();
75
+ reject(new NotConnectedError());
76
+ };
77
+ socket.onerror = () => {
78
+ clearTimeout(timer);
79
+ this.dropSocket();
80
+ reject(new NotConnectedError());
81
+ };
82
+ socket.onmessage = (ev) => this.onMessage(ev);
83
+ });
84
+ this.connecting = attempt;
85
+ const clear = () => {
86
+ if (this.connecting === attempt) this.connecting = null;
87
+ };
88
+ attempt.then(clear, clear);
89
+ return attempt;
90
+ }
91
+ dropSocket() {
92
+ if (this.socket) {
93
+ this.socket.onopen = null;
94
+ this.socket.onclose = null;
95
+ this.socket.onerror = null;
96
+ this.socket.onmessage = null;
97
+ try {
98
+ this.socket.close();
99
+ } catch {}
100
+ this.socket = null;
101
+ }
102
+ for (const [, p] of this.pending) {
103
+ clearTimeout(p.timer);
104
+ p.reject(new NotConnectedError());
105
+ }
106
+ this.pending.clear();
107
+ }
108
+ onMessage(ev) {
109
+ let reply;
110
+ try {
111
+ reply = JSON.parse(ev.data);
112
+ } catch {
113
+ return;
114
+ }
115
+ const p = reply?.id !== void 0 ? this.pending.get(reply.id) : void 0;
116
+ if (!p) return;
117
+ this.pending.delete(reply.id);
118
+ clearTimeout(p.timer);
119
+ p.resolve(reply);
120
+ }
121
+ /**
122
+ * Sends one command and awaits its correlated reply. Connection problems
123
+ * and timeouts both surface as NotConnectedError with the user-facing text.
124
+ */
125
+ async send(command, params = {}) {
126
+ await this.ensureSocket();
127
+ const id = this.nextId++;
128
+ const frame = JSON.stringify({
129
+ id,
130
+ command,
131
+ ...params
132
+ });
133
+ return new Promise((resolve, reject) => {
134
+ const timer = setTimeout(() => {
135
+ this.pending.delete(id);
136
+ reject(new NotConnectedError());
137
+ }, REQUEST_TIMEOUT_MS);
138
+ this.pending.set(id, {
139
+ resolve: (reply) => reply.ok ? resolve(reply.result) : reject(new Error(reply.error ?? "bridge error")),
140
+ reject,
141
+ timer
142
+ });
143
+ try {
144
+ this.socket.send(frame);
145
+ } catch (e) {
146
+ this.pending.delete(id);
147
+ clearTimeout(timer);
148
+ reject(new NotConnectedError());
149
+ }
150
+ });
151
+ }
152
+ close() {
153
+ this.dropSocket();
154
+ }
155
+ };
156
+
157
+ //#endregion
158
+ //#region src/browser-mcp/tools.ts
159
+ const tabSchema = number().int().describe("Tab id, as returned by tabs_list.");
160
+ function asRecord(v) {
161
+ return v ?? {};
162
+ }
163
+ function formatTabs(result) {
164
+ const tabs = Array.isArray(result) ? result : asRecord(result).tabs;
165
+ if (!Array.isArray(tabs) || tabs.length === 0) return "(no open tabs)";
166
+ return tabs.map((t) => `${t.id}${t.active ? " *" : " "} ${String(t.title ?? "")}\n ${String(t.url ?? "")}`).join("\n");
167
+ }
168
+ function formatSnapshot(result) {
169
+ return String(asRecord(result).yaml ?? "");
170
+ }
171
+ function formatLogs(result) {
172
+ const entries = asRecord(result).entries;
173
+ if (!Array.isArray(entries) || entries.length === 0) return "(no console output captured)";
174
+ return entries.map((e) => {
175
+ return `[${e.source === "console" ? String(e.type ?? "log") : String(e.level ?? "log")}] ${String(e.text ?? "")}`;
176
+ }).join("\n");
177
+ }
178
+ function formatEval(result) {
179
+ const value = asRecord(result).value;
180
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
181
+ }
182
+ function formatScreenshot(result) {
183
+ return String(asRecord(result).base64 ?? "");
184
+ }
185
+ const BROWSER_TOOLS = [
186
+ {
187
+ name: "tabs_list",
188
+ description: "List the tabs of the user's real running Chrome (id, title, url, active).",
189
+ shape: {},
190
+ command: "list_tabs",
191
+ toParams: () => ({}),
192
+ format: formatTabs
193
+ },
194
+ {
195
+ name: "tab_open",
196
+ description: "Open a new tab in the user's Chrome and return its tab id.",
197
+ shape: {
198
+ url: string().describe("URL to open."),
199
+ active: boolean().optional().describe("Focus the new tab (default true).")
200
+ },
201
+ command: "open_tab",
202
+ toParams: (a) => ({
203
+ url: a.url,
204
+ ...a.active !== void 0 ? { active: a.active } : {}
205
+ })
206
+ },
207
+ {
208
+ name: "tab_select",
209
+ description: "Bring a tab to the front (activate it and focus its window).",
210
+ shape: { tab: tabSchema },
211
+ command: "select_tab",
212
+ toParams: (a) => ({ tabId: a.tab })
213
+ },
214
+ {
215
+ name: "tab_close",
216
+ description: "Close a tab.",
217
+ shape: { tab: tabSchema },
218
+ command: "close_tab",
219
+ toParams: (a) => ({ tabId: a.tab })
220
+ },
221
+ {
222
+ name: "dom_snapshot",
223
+ description: "Snapshot a tab's DOM as an accessibility-tree YAML with [ref=sN] ids for interactive elements. Take a snapshot before dom_click/dom_type; refs stay valid until the next snapshot or navigation.",
224
+ shape: { tab: tabSchema },
225
+ command: "snapshot",
226
+ toParams: (a) => ({ tabId: a.tab }),
227
+ format: formatSnapshot
228
+ },
229
+ {
230
+ name: "dom_click",
231
+ description: "Click an element by its [ref=sN] from the latest dom_snapshot (trusted CDP event).",
232
+ shape: {
233
+ tab: tabSchema,
234
+ ref: string().describe("Ref id from dom_snapshot, e.g. s3.")
235
+ },
236
+ command: "click",
237
+ toParams: (a) => ({
238
+ tabId: a.tab,
239
+ ref: a.ref
240
+ })
241
+ },
242
+ {
243
+ name: "dom_type",
244
+ description: "Focus an element by its [ref=sN] and type text into it (inserts at the caret, works in SPAs).",
245
+ shape: {
246
+ tab: tabSchema,
247
+ ref: string().describe("Ref id from dom_snapshot, e.g. s5."),
248
+ text: string().describe("Text to type.")
249
+ },
250
+ command: "type",
251
+ toParams: (a) => ({
252
+ tabId: a.tab,
253
+ ref: a.ref,
254
+ text: a.text
255
+ })
256
+ },
257
+ {
258
+ name: "page_text",
259
+ description: "Read a tab's visible text (document.body.innerText).",
260
+ shape: { tab: tabSchema },
261
+ command: "eval",
262
+ toParams: () => ({ code: "document.body.innerText" }),
263
+ format: formatEval
264
+ },
265
+ {
266
+ name: "eval_js",
267
+ description: "Evaluate JavaScript in a tab and return the JSON-safe result. Awaits promises.",
268
+ shape: {
269
+ tab: tabSchema,
270
+ code: string().describe("JavaScript expression to evaluate.")
271
+ },
272
+ command: "eval",
273
+ toParams: (a) => ({ code: a.code }),
274
+ format: formatEval
275
+ },
276
+ {
277
+ name: "tab_screenshot",
278
+ description: "Screenshot a tab (PNG, base64).",
279
+ shape: { tab: tabSchema },
280
+ command: "screenshot",
281
+ toParams: (a) => ({ tabId: a.tab }),
282
+ format: formatScreenshot
283
+ },
284
+ {
285
+ name: "console_logs",
286
+ description: "Read the console output captured for a tab since its last snapshot.",
287
+ shape: { tab: tabSchema },
288
+ command: "console_logs",
289
+ toParams: (a) => ({ tabId: a.tab }),
290
+ format: formatLogs
291
+ }
292
+ ];
293
+ /** Runs one tool against the bridge; returns MCP content + isError. */
294
+ async function runBrowserTool(def, args, bridge) {
295
+ try {
296
+ const result = await bridge.send(def.command, def.toParams(args));
297
+ return { content: [{
298
+ type: "text",
299
+ text: def.format ? def.format(result) : JSON.stringify(result, null, 2)
300
+ }] };
301
+ } catch (e) {
302
+ return {
303
+ content: [{
304
+ type: "text",
305
+ text: e instanceof NotConnectedError ? e.message : e instanceof Error ? e.message : String(e)
306
+ }],
307
+ isError: true
308
+ };
309
+ }
310
+ }
311
+ /** Registers every browser tool on an MCP server. */
312
+ function registerBrowserTools(server, bridge) {
313
+ for (const def of BROWSER_TOOLS) server.tool(def.name, def.description, def.shape, (args) => runBrowserTool(def, args, bridge));
314
+ }
315
+
316
+ //#endregion
317
+ //#region src/browser-mcp/index.ts
318
+ /**
319
+ * pai-browser-mcp — stdio MCP server for the PAI browser bridge.
320
+ *
321
+ * The provider-independent claude-in-chrome replacement: drives the user's
322
+ * real running Chrome (tabs + DOM) through the browser-bridge extension's
323
+ * native messaging host, over ws://127.0.0.1:8756. Any MCP client works —
324
+ * nothing here is Anthropic-specific.
325
+ *
326
+ * provider → MCP (this server) → WebSocket → native host → extension → Chrome
327
+ *
328
+ * If Chrome is not running (or the extension is not loaded), every tool
329
+ * returns a clear "start Chrome" error; the server itself stays alive and
330
+ * reconnects lazily per call.
331
+ */
332
+ async function main() {
333
+ if (globalThis.WebSocket === void 0) {
334
+ process.stderr.write("pai-browser-mcp: this node runtime has no global WebSocket — run with node >= 22\n");
335
+ process.exit(1);
336
+ }
337
+ const server = new McpServer({
338
+ name: "pai-browser",
339
+ version: "0.1.0"
340
+ });
341
+ registerBrowserTools(server, new BridgeClient(void 0, globalWebSocketFactory));
342
+ const transport = new StdioServerTransport();
343
+ await server.connect(transport);
344
+ }
345
+ main().catch((e) => {
346
+ process.stderr.write(`pai-browser-mcp fatal error: ${String(e)}\n`);
347
+ process.exit(1);
348
+ });
349
+
350
+ //#endregion
351
+ export { };
352
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["z.number","z.string","z.boolean"],"sources":["../../src/browser-mcp/bridge-client.ts","../../src/browser-mcp/tools.ts","../../src/browser-mcp/index.ts"],"sourcesContent":["/**\n * WebSocket client for the browser bridge.\n *\n * Talks to the native host's hand-rolled WS server (ws://127.0.0.1:8756) with\n * the global WebSocket — node >= 22, no external dependency. Requests carry an\n * id; replies are correlated by it. The socket is connected lazily per call\n * and reconnected on demand, so the server survives Chrome being closed.\n */\n\nexport const BRIDGE_PORT = Number(process.env.PAI_BROWSER_BRIDGE_PORT || 8756);\nexport const BRIDGE_URL = `ws://127.0.0.1:${BRIDGE_PORT}`;\n\nexport const NOT_CONNECTED_MESSAGE =\n \"Chrome bridge not connected — start Chrome (the PAI browser-bridge extension must be running) and try again.\";\n\n/** Minimal shape of the pieces of WebSocket this client uses. */\nexport interface WebSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n onopen: (() => void) | null;\n onclose: (() => void) | null;\n onerror: (() => void) | null;\n onmessage: ((ev: { data: string }) => void) | null;\n}\n\nexport type WebSocketFactory = (url: string) => WebSocketLike;\n\nexport const globalWebSocketFactory: WebSocketFactory = (url) => {\n const WS = (globalThis as { WebSocket?: new (url: string) => WebSocketLike }).WebSocket;\n if (!WS) {\n throw new Error(\n \"this node runtime has no global WebSocket — pai-browser-mcp needs node >= 22\"\n );\n }\n return new WS(url);\n};\n\nexport class NotConnectedError extends Error {\n constructor() {\n super(NOT_CONNECTED_MESSAGE);\n this.name = \"NotConnectedError\";\n }\n}\n\nexport interface BridgeReply {\n id: number;\n ok: boolean;\n result?: unknown;\n error?: string;\n}\n\nconst CONNECT_TIMEOUT_MS = 5_000;\nconst REQUEST_TIMEOUT_MS = 5_000;\nconst OPEN = 1;\n\nexport class BridgeClient {\n private socket: WebSocketLike | null = null;\n private connecting: Promise<WebSocketLike> | null = null;\n private nextId = 1;\n private readonly pending = new Map<\n number,\n { resolve: (r: BridgeReply) => void; reject: (e: Error) => void; timer: NodeJS.Timeout }\n >();\n\n constructor(\n private readonly url: string = BRIDGE_URL,\n private readonly socketFactory: WebSocketFactory = globalWebSocketFactory\n ) {}\n\n /** True when the underlying socket is open. */\n get connected(): boolean {\n return this.socket !== null && this.socket.readyState === OPEN;\n }\n\n private ensureSocket(): Promise<WebSocketLike> {\n if (this.socket && this.socket.readyState === OPEN) return Promise.resolve(this.socket);\n if (this.connecting) return this.connecting; // concurrent sends share one socket\n this.dropSocket();\n const attempt = new Promise<WebSocketLike>((resolve, reject) => {\n let socket: WebSocketLike;\n try {\n socket = this.socketFactory(this.url);\n } catch (e) {\n reject(e);\n return;\n }\n const timer = setTimeout(() => {\n reject(new NotConnectedError());\n socket.onopen = null;\n socket.onclose = null;\n socket.onerror = null;\n socket.onmessage = null;\n try {\n socket.close();\n } catch {\n /* already gone */\n }\n }, CONNECT_TIMEOUT_MS);\n socket.onopen = () => {\n clearTimeout(timer);\n this.socket = socket;\n resolve(socket);\n };\n socket.onclose = () => {\n clearTimeout(timer);\n this.dropSocket();\n reject(new NotConnectedError());\n };\n socket.onerror = () => {\n clearTimeout(timer);\n this.dropSocket();\n reject(new NotConnectedError());\n };\n socket.onmessage = (ev) => this.onMessage(ev);\n });\n this.connecting = attempt;\n const clear = () => {\n if (this.connecting === attempt) this.connecting = null;\n };\n attempt.then(clear, clear);\n return attempt;\n }\n\n private dropSocket(): void {\n if (this.socket) {\n this.socket.onopen = null;\n this.socket.onclose = null;\n this.socket.onerror = null;\n this.socket.onmessage = null;\n try {\n this.socket.close();\n } catch {\n /* already gone */\n }\n this.socket = null;\n }\n // Fail everything still waiting — the peer is gone.\n for (const [, p] of this.pending) {\n clearTimeout(p.timer);\n p.reject(new NotConnectedError());\n }\n this.pending.clear();\n }\n\n private onMessage(ev: { data: string }): void {\n let reply: BridgeReply;\n try {\n reply = JSON.parse(ev.data) as BridgeReply;\n } catch {\n return; // bridge noise (keepalive pings echo nothing back); ignore\n }\n const p = reply?.id !== undefined ? this.pending.get(reply.id) : undefined;\n if (!p) return;\n this.pending.delete(reply.id);\n clearTimeout(p.timer);\n p.resolve(reply);\n }\n\n /**\n * Sends one command and awaits its correlated reply. Connection problems\n * and timeouts both surface as NotConnectedError with the user-facing text.\n */\n async send(command: string, params: Record<string, unknown> = {}): Promise<unknown> {\n await this.ensureSocket();\n const id = this.nextId++;\n const frame = JSON.stringify({ id, command, ...params });\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n reject(new NotConnectedError());\n }, REQUEST_TIMEOUT_MS);\n this.pending.set(id, {\n resolve: (reply) => (reply.ok ? resolve(reply.result) : reject(new Error(reply.error ?? \"bridge error\"))),\n reject,\n timer,\n });\n try {\n this.socket!.send(frame);\n } catch (e) {\n this.pending.delete(id);\n clearTimeout(timer);\n reject(new NotConnectedError());\n }\n });\n }\n\n close(): void {\n this.dropSocket();\n }\n}\n","/**\n * pai-browser MCP tool table.\n *\n * One definition per tool: MCP-facing name/schema, the wire command it maps\n * to on the browser bridge, param translation, and result formatting. Kept as\n * a data table so tests can drive every mapping without a transport, and so\n * registerBrowserTools stays a five-line loop.\n */\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport { NotConnectedError } from \"./bridge-client.js\";\n\nexport interface BridgeLike {\n send(command: string, params?: Record<string, unknown>): Promise<unknown>;\n}\n\nexport interface BrowserToolDef {\n name: string;\n description: string;\n /** zod raw shape for server.tool() */\n shape: z.ZodRawShape;\n /** wire command the extension handles */\n command: string;\n /** MCP args → wire params */\n toParams: (args: Record<string, unknown>) => Record<string, unknown>;\n /** wire result → text shown to the model */\n format?: (result: unknown) => string;\n}\n\nconst tabSchema = z.number().int().describe(\"Tab id, as returned by tabs_list.\");\n\nfunction asRecord(v: unknown): Record<string, unknown> {\n return (v ?? {}) as Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Result formatters\n// ---------------------------------------------------------------------------\n\nfunction formatTabs(result: unknown): string {\n const tabs = (Array.isArray(result) ? result : asRecord(result).tabs) as Array<\n Record<string, unknown>\n >;\n if (!Array.isArray(tabs) || tabs.length === 0) return \"(no open tabs)\";\n return tabs\n .map(\n (t) =>\n `${t.id}${t.active ? \" *\" : \" \"} ${String(t.title ?? \"\")}\\n ${String(t.url ?? \"\")}`\n )\n .join(\"\\n\");\n}\n\nfunction formatSnapshot(result: unknown): string {\n return String(asRecord(result).yaml ?? \"\");\n}\n\nfunction formatLogs(result: unknown): string {\n const entries = asRecord(result).entries as Array<Record<string, unknown>>;\n if (!Array.isArray(entries) || entries.length === 0) return \"(no console output captured)\";\n return entries\n .map((e) => {\n const kind = e.source === \"console\" ? String(e.type ?? \"log\") : String(e.level ?? \"log\");\n return `[${kind}] ${String(e.text ?? \"\")}`;\n })\n .join(\"\\n\");\n}\n\nfunction formatEval(result: unknown): string {\n const value = asRecord(result).value;\n return typeof value === \"string\" ? value : JSON.stringify(value, null, 2);\n}\n\nfunction formatScreenshot(result: unknown): string {\n // Raw base64 PNG — the tool contract is \"base64 png\", not prose.\n return String(asRecord(result).base64 ?? \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Tool table: MCP name → wire command\n// ---------------------------------------------------------------------------\n\nexport const BROWSER_TOOLS: BrowserToolDef[] = [\n {\n name: \"tabs_list\",\n description: \"List the tabs of the user's real running Chrome (id, title, url, active).\",\n shape: {},\n command: \"list_tabs\",\n toParams: () => ({}),\n format: formatTabs,\n },\n {\n name: \"tab_open\",\n description: \"Open a new tab in the user's Chrome and return its tab id.\",\n shape: {\n url: z.string().describe(\"URL to open.\"),\n active: z.boolean().optional().describe(\"Focus the new tab (default true).\"),\n },\n command: \"open_tab\",\n toParams: (a) => ({ url: a.url, ...(a.active !== undefined ? { active: a.active } : {}) }),\n },\n {\n name: \"tab_select\",\n description: \"Bring a tab to the front (activate it and focus its window).\",\n shape: { tab: tabSchema },\n command: \"select_tab\",\n toParams: (a) => ({ tabId: a.tab }),\n },\n {\n name: \"tab_close\",\n description: \"Close a tab.\",\n shape: { tab: tabSchema },\n command: \"close_tab\",\n toParams: (a) => ({ tabId: a.tab }),\n },\n {\n name: \"dom_snapshot\",\n description:\n \"Snapshot a tab's DOM as an accessibility-tree YAML with [ref=sN] ids for interactive elements. \" +\n \"Take a snapshot before dom_click/dom_type; refs stay valid until the next snapshot or navigation.\",\n shape: { tab: tabSchema },\n command: \"snapshot\",\n toParams: (a) => ({ tabId: a.tab }),\n format: formatSnapshot,\n },\n {\n name: \"dom_click\",\n description: \"Click an element by its [ref=sN] from the latest dom_snapshot (trusted CDP event).\",\n shape: { tab: tabSchema, ref: z.string().describe(\"Ref id from dom_snapshot, e.g. s3.\") },\n command: \"click\",\n toParams: (a) => ({ tabId: a.tab, ref: a.ref }),\n },\n {\n name: \"dom_type\",\n description:\n \"Focus an element by its [ref=sN] and type text into it (inserts at the caret, works in SPAs).\",\n shape: {\n tab: tabSchema,\n ref: z.string().describe(\"Ref id from dom_snapshot, e.g. s5.\"),\n text: z.string().describe(\"Text to type.\"),\n },\n command: \"type\",\n toParams: (a) => ({ tabId: a.tab, ref: a.ref, text: a.text }),\n },\n {\n name: \"page_text\",\n description: \"Read a tab's visible text (document.body.innerText).\",\n shape: { tab: tabSchema },\n command: \"eval\",\n toParams: () => ({ code: \"document.body.innerText\" }),\n format: formatEval,\n },\n {\n name: \"eval_js\",\n description: \"Evaluate JavaScript in a tab and return the JSON-safe result. Awaits promises.\",\n shape: {\n tab: tabSchema,\n code: z.string().describe(\"JavaScript expression to evaluate.\"),\n },\n command: \"eval\",\n toParams: (a) => ({ code: a.code }),\n format: formatEval,\n },\n {\n name: \"tab_screenshot\",\n description: \"Screenshot a tab (PNG, base64).\",\n shape: { tab: tabSchema },\n command: \"screenshot\",\n toParams: (a) => ({ tabId: a.tab }),\n format: formatScreenshot,\n },\n {\n name: \"console_logs\",\n description: \"Read the console output captured for a tab since its last snapshot.\",\n shape: { tab: tabSchema },\n command: \"console_logs\",\n toParams: (a) => ({ tabId: a.tab }),\n format: formatLogs,\n },\n];\n\n/** Runs one tool against the bridge; returns MCP content + isError. */\nexport async function runBrowserTool(\n def: BrowserToolDef,\n args: Record<string, unknown>,\n bridge: BridgeLike\n): Promise<{ content: Array<{ type: \"text\"; text: string }>; isError?: boolean }> {\n try {\n const result = await bridge.send(def.command, def.toParams(args));\n return { content: [{ type: \"text\", text: def.format ? def.format(result) : JSON.stringify(result, null, 2) }] };\n } catch (e) {\n const msg =\n e instanceof NotConnectedError\n ? e.message\n : e instanceof Error\n ? e.message\n : String(e);\n return { content: [{ type: \"text\", text: msg }], isError: true };\n }\n}\n\n/** Registers every browser tool on an MCP server. */\nexport function registerBrowserTools(server: McpServer, bridge: BridgeLike): void {\n for (const def of BROWSER_TOOLS) {\n server.tool(def.name, def.description, def.shape, (args) => runBrowserTool(def, args, bridge));\n }\n}\n","#!/usr/bin/env node\n/**\n * pai-browser-mcp — stdio MCP server for the PAI browser bridge.\n *\n * The provider-independent claude-in-chrome replacement: drives the user's\n * real running Chrome (tabs + DOM) through the browser-bridge extension's\n * native messaging host, over ws://127.0.0.1:8756. Any MCP client works —\n * nothing here is Anthropic-specific.\n *\n * provider → MCP (this server) → WebSocket → native host → extension → Chrome\n *\n * If Chrome is not running (or the extension is not loaded), every tool\n * returns a clear \"start Chrome\" error; the server itself stays alive and\n * reconnects lazily per call.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { BridgeClient, globalWebSocketFactory } from \"./bridge-client.js\";\nimport { registerBrowserTools } from \"./tools.js\";\n\nasync function main(): Promise<void> {\n if (globalThis.WebSocket === undefined) {\n // Node < 22 has no global WebSocket client; say so instead of crashing\n // with an opaque TypeError when the first tool call connects.\n process.stderr.write(\n \"pai-browser-mcp: this node runtime has no global WebSocket — run with node >= 22\\n\"\n );\n process.exit(1);\n }\n\n const server = new McpServer({\n name: \"pai-browser\",\n version: \"0.1.0\",\n });\n\n const bridge = new BridgeClient(undefined, globalWebSocketFactory);\n registerBrowserTools(server, bridge);\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((e) => {\n process.stderr.write(`pai-browser-mcp fatal error: ${String(e)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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"}
@@ -17,6 +17,7 @@ function parseRunnerArgs(argv) {
17
17
  let callerMcpConfig = false;
18
18
  let callerSystemPrompt = false;
19
19
  const mcp = [];
20
+ const allowedTools = [];
20
21
  let i = 0;
21
22
  while (i < argv.length) {
22
23
  const a = argv[i];
@@ -42,7 +43,18 @@ function parseRunnerArgs(argv) {
42
43
  i += 1;
43
44
  }
44
45
  } else if (a.startsWith("--mcp=")) mcp.push(a.slice(6));
45
- else {
46
+ else if (a === "--allowedTools") {
47
+ rest.push(a);
48
+ const v = argv[i + 1];
49
+ if (v !== void 0 && !v.startsWith("-")) {
50
+ allowedTools.push(v);
51
+ rest.push(v);
52
+ i += 1;
53
+ }
54
+ } else if (a.startsWith("--allowedTools=")) {
55
+ allowedTools.push(a.slice(15));
56
+ rest.push(a);
57
+ } else {
46
58
  if (a === "--model") callerModel = true;
47
59
  if (a.startsWith("--model=")) callerModel = true;
48
60
  if (a === "--mcp-config") callerMcpConfig = true;
@@ -62,7 +74,8 @@ function parseRunnerArgs(argv) {
62
74
  callerModel,
63
75
  callerMcpConfig,
64
76
  callerSystemPrompt,
65
- mcp
77
+ mcp,
78
+ allowedTools
66
79
  };
67
80
  }
68
81
  /**
@@ -1607,9 +1620,11 @@ function relShort(p, cwd) {
1607
1620
  * Headless workers start with NO MCP servers: every server definition is a
1608
1621
  * prompt-time tool inventory the model pays to know, and a worker that only
1609
1622
  * reads and edits files needs none of it. A run may opt in with `--mcp
1610
- * name[,name…]` (or a role carrying `"mcp": [...]`); names may be single
1611
- * servers from ~/.claude.json's `mcpServers` or `workers.mcpSets` set names,
1612
- * which expand to their member list. The filtered config lands in
1623
+ * name[,name…]`, a role carrying `"mcp": [...]`, or implicitly by naming
1624
+ * `mcp__server__tool` in --allowedTools (a grant without its server loaded is
1625
+ * a dead letter). Names may be single servers from ~/.claude.json's
1626
+ * `mcpServers` or `workers.mcpSets` set names, which expand to their member
1627
+ * list. The filtered config lands in
1613
1628
  * `<logDir>/<id>.mcp.json` and is passed with `--strict-mcp-config
1614
1629
  * --mcp-config` so exactly those servers load. MCP servers are chosen at
1615
1630
  * launch only — a mid-run `say` cannot add any.
@@ -1646,6 +1661,22 @@ function expandMcpNames(names, config, claudeJson = CLAUDE_JSON) {
1646
1661
  }
1647
1662
  return out;
1648
1663
  }
1664
+ /**
1665
+ * Derive server names from tool grants: every `mcp__<server>__<tool>` (or bare
1666
+ * `mcp__<server>`, or `mcp__<server>__*`) in an --allowedTools list names a
1667
+ * server the run expects to be loaded. A grant is a dead letter unless its
1668
+ * server is in the filtered config, so the runner treats these as implicit
1669
+ * --mcp names — and only these; non-mcp grants load nothing.
1670
+ */
1671
+ function mcpServersFromToolGrants(tools) {
1672
+ const out = [];
1673
+ for (const entry of tools) for (const name of entry.split(",").map((s) => s.trim()).filter(Boolean)) {
1674
+ if (!name.startsWith("mcp__")) continue;
1675
+ const server = name.slice(5).split("__")[0];
1676
+ if (server && !out.includes(server)) out.push(server);
1677
+ }
1678
+ return out;
1679
+ }
1649
1680
  /** Path of a run's filtered MCP config. */
1650
1681
  function runMcpConfigPath(logDir, id) {
1651
1682
  return join(logDir, `${id}.mcp.json`);
@@ -1961,7 +1992,7 @@ async function runWorker(opts) {
1961
1992
  const logDir = workersLogDir(config);
1962
1993
  mkdirSync(logDir, { recursive: true });
1963
1994
  if (opts.className === "plan" && !opts._planner) {
1964
- const { runPlanner } = await import("./planner-Cm3g6fWH.mjs");
1995
+ const { runPlanner } = await import("./planner-DsmMRIZR.mjs");
1965
1996
  return runPlanner(opts);
1966
1997
  }
1967
1998
  const parent = launchParent(opts.parent);
@@ -2093,7 +2124,8 @@ async function executeRun(a) {
2093
2124
  const wanted = [
2094
2125
  ...a.mcpFlag ? [a.mcpFlag] : [],
2095
2126
  ...parsed.mcp,
2096
- ...target.classMcp ?? []
2127
+ ...target.classMcp ?? [],
2128
+ ...mcpServersFromToolGrants(parsed.allowedTools)
2097
2129
  ];
2098
2130
  if (wanted.length) mcpArgs = [
2099
2131
  "--strict-mcp-config",
@@ -2794,4 +2826,4 @@ async function runChain(opts, deps = {}) {
2794
2826
 
2795
2827
  //#endregion
2796
2828
  export { appendLedger as A, ageOf as C, loadStatus as D, contextPercent as E, parseRunnerArgs as M, shortText as N, loadStatuses as O, UNLABELED as S, contextLabel as T, openPaneForWorker as _, testProvider as a, sessionTag as b, renderReport as c, handoffFromInside as d, readInbox as f, openFollowPane as g, checkPaneForWorker as h, runWorker as i, ledgerSummary as j, newWorkerId as k, discardWorker as l, workerDepth as m, swapPromptArg as n, describeMcp as o, sayToWorker as p, printResult as r, parseWorkerReport as s, runChain as t, mergeWorker as u, currentTabKey as v, alive as w, workerInScope as x, resolveSession as y };
2797
- //# sourceMappingURL=chain-DhVHVnmT.mjs.map
2829
+ //# sourceMappingURL=chain-zMjCDW-R.mjs.map