pi-browser-use 0.9.6 → 0.10.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.
@@ -0,0 +1,47 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { type BrowserRuntime, type BrowserRuntimeOptions } from './runtime.js';
3
+ /** Only explicit host configuration is read; never .pi files or project settings. */
4
+ export declare function loadPortableOptions(env?: NodeJS.ProcessEnv): BrowserRuntimeOptions;
5
+ /** Low-level MCP adapter preserves the runtime's JSON schemas and curated tools. */
6
+ export declare function createBrowserMcpServer(options?: {
7
+ runtime?: BrowserRuntime;
8
+ env?: NodeJS.ProcessEnv;
9
+ }): {
10
+ server: Server<{
11
+ method: string;
12
+ params?: {
13
+ [x: string]: unknown;
14
+ _meta?: {
15
+ [x: string]: unknown;
16
+ progressToken?: string | number | undefined;
17
+ "io.modelcontextprotocol/related-task"?: {
18
+ taskId: string;
19
+ } | undefined;
20
+ } | undefined;
21
+ } | undefined;
22
+ }, {
23
+ method: string;
24
+ params?: {
25
+ [x: string]: unknown;
26
+ _meta?: {
27
+ [x: string]: unknown;
28
+ progressToken?: string | number | undefined;
29
+ "io.modelcontextprotocol/related-task"?: {
30
+ taskId: string;
31
+ } | undefined;
32
+ } | undefined;
33
+ } | undefined;
34
+ }, {
35
+ [x: string]: unknown;
36
+ _meta?: {
37
+ [x: string]: unknown;
38
+ progressToken?: string | number | undefined;
39
+ "io.modelcontextprotocol/related-task"?: {
40
+ taskId: string;
41
+ } | undefined;
42
+ } | undefined;
43
+ }>;
44
+ close: () => Promise<void>;
45
+ };
46
+ export declare function runStdio(): Promise<void>;
47
+ //# sourceMappingURL=mcp-server.d.ts.map
@@ -0,0 +1,218 @@
1
+ import { mkdirSync, readFileSync, realpathSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { isAbsolute, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { CallToolRequestSchema, CallToolResultSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
8
+ import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv-provider.js';
9
+ import { createBrowserRuntime } from './runtime.js';
10
+ import { expandHome } from './config.js';
11
+ const validator = new AjvJsonSchemaValidator();
12
+ const booleanOptions = [
13
+ 'headed',
14
+ 'headless',
15
+ 'isolated',
16
+ 'autoConnect',
17
+ 'categoryPerformance',
18
+ 'categoryNetwork',
19
+ 'categoryEmulation',
20
+ 'categoryExtensions',
21
+ 'experimentalVision',
22
+ 'experimentalScreencast',
23
+ 'experimentalMemory',
24
+ 'experimentalPageIdRouting',
25
+ 'usageStatistics',
26
+ 'performanceCrux',
27
+ 'redactNetworkHeaders',
28
+ 'acceptInsecureCerts',
29
+ 'slim',
30
+ ];
31
+ const stringOptions = [
32
+ 'browserUrl',
33
+ 'wsEndpoint',
34
+ 'wsHeaders',
35
+ 'executablePath',
36
+ 'viewport',
37
+ 'userDataDir',
38
+ ];
39
+ const arrayOptions = ['allowedUrlPattern', 'blockedUrlPattern', 'chromeArgs', 'extraArgs'];
40
+ const validateConfig = validator.getValidator({
41
+ type: 'object',
42
+ additionalProperties: false,
43
+ properties: {
44
+ ...Object.fromEntries(booleanOptions.map((key) => [key, { type: 'boolean' }])),
45
+ ...Object.fromEntries(stringOptions.map((key) => [key, { type: 'string', minLength: 1 }])),
46
+ ...Object.fromEntries(arrayOptions.map((key) => [key, { type: 'array', items: { type: 'string' } }])),
47
+ mode: { enum: ['fresh', 'persistent', 'existing'] },
48
+ sessionMode: { enum: ['isolated', 'persistent', 'existing'] },
49
+ channel: { enum: ['canary', 'dev', 'beta', 'stable'] },
50
+ tabBridgePort: { type: 'integer', minimum: 0, maximum: 65535 },
51
+ },
52
+ });
53
+ /** Only explicit host configuration is read; never .pi files or project settings. */
54
+ export function loadPortableOptions(env = process.env) {
55
+ const dataDir = env.PI_BROWSER_USE_DATA_DIR ??
56
+ env.PLUGIN_DATA ??
57
+ join(homedir(), '.local', 'share', 'pi-browser-use');
58
+ if (!isAbsolute(dataDir))
59
+ throw new Error('Browser plugin data directory must be absolute.');
60
+ const configFile = env.PI_BROWSER_USE_CONFIG ?? join(dataDir, 'config.json');
61
+ if (!isAbsolute(configFile))
62
+ throw new Error('PI_BROWSER_USE_CONFIG must be an absolute path.');
63
+ let raw;
64
+ try {
65
+ raw = readFileSync(configFile, 'utf8');
66
+ }
67
+ catch (error) {
68
+ if (error.code !== 'ENOENT' || env.PI_BROWSER_USE_CONFIG)
69
+ throw error;
70
+ }
71
+ let parsed = {};
72
+ if (raw !== undefined) {
73
+ try {
74
+ parsed = JSON.parse(raw);
75
+ }
76
+ catch (error) {
77
+ // JSON parse errors may include fragments of wsHeaders. Never echo those.
78
+ throw new Error('Browser plugin config.json is not valid JSON.', { cause: error });
79
+ }
80
+ }
81
+ const checked = validateConfig(parsed);
82
+ if (!checked.valid) {
83
+ throw new Error('Invalid browser plugin config.json. Use documented BrowserUseConfig fields; visionModel is Pi-only.');
84
+ }
85
+ const config = checked.data;
86
+ for (const key of ['userDataDir', 'executablePath']) {
87
+ if (config[key]) {
88
+ config[key] = expandHome(config[key]);
89
+ if (!isAbsolute(config[key]))
90
+ throw new Error(`${key} must be an absolute path (or start with ~/).`);
91
+ }
92
+ }
93
+ mkdirSync(dataDir, { recursive: true, mode: 0o700 });
94
+ return {
95
+ config,
96
+ defaultProfileDir: join(dataDir, 'browser-profile'),
97
+ artifactDir: join(dataDir, 'artifacts'),
98
+ lazyBrowser: true,
99
+ // Other hosts analyze returned screenshot images using their own model.
100
+ // No implicit Pi credentials, sampling calls, or extra model charges.
101
+ visionEnabled: false,
102
+ };
103
+ }
104
+ function packageVersion() {
105
+ return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
106
+ }
107
+ function reportFailure(error) {
108
+ console.error(`[pi-browser-use] MCP lifecycle failed (${error instanceof Error ? error.name : 'UnknownError'}).`);
109
+ }
110
+ /** Low-level MCP adapter preserves the runtime's JSON schemas and curated tools. */
111
+ export function createBrowserMcpServer(options = {}) {
112
+ const runtime = options.runtime ?? createBrowserRuntime(loadPortableOptions(options.env));
113
+ const server = new Server({ name: 'pi-browser-use', version: packageVersion() }, { capabilities: { tools: {} } });
114
+ const validators = new Map();
115
+ let closePromise;
116
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
117
+ const tools = await runtime.start();
118
+ return {
119
+ tools: tools.map((tool) => ({
120
+ name: tool.name,
121
+ description: tool.description,
122
+ inputSchema: tool.parameters,
123
+ })),
124
+ };
125
+ });
126
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
127
+ extra.signal.throwIfAborted();
128
+ const tools = await runtime.start();
129
+ extra.signal.throwIfAborted();
130
+ const tool = tools.find((candidate) => candidate.name === request.params.name);
131
+ if (!tool)
132
+ throw new McpError(ErrorCode.InvalidParams, `Unknown browser tool: ${request.params.name}`);
133
+ let validate = validators.get(tool.name);
134
+ if (!validate) {
135
+ validate = validator.getValidator(tool.parameters);
136
+ validators.set(tool.name, validate);
137
+ }
138
+ const checked = validate(request.params.arguments ?? {});
139
+ if (!checked.valid) {
140
+ // Do not echo argument values (forms may contain sensitive content).
141
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${tool.name}. Follow its inputSchema.`);
142
+ }
143
+ try {
144
+ const result = await tool.execute(checked.data, extra.signal);
145
+ return CallToolResultSchema.parse({
146
+ content: result.content ?? [],
147
+ ...(result.isError ? { isError: true } : {}),
148
+ });
149
+ }
150
+ catch (error) {
151
+ if (extra.signal.aborted)
152
+ throw error;
153
+ return {
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ text: error instanceof Error ? error.message : 'Browser operation failed.',
158
+ },
159
+ ],
160
+ isError: true,
161
+ };
162
+ }
163
+ });
164
+ async function shutdown() {
165
+ try {
166
+ await runtime.stop();
167
+ }
168
+ finally {
169
+ await server.close();
170
+ }
171
+ }
172
+ function close() {
173
+ closePromise ??= shutdown();
174
+ return closePromise;
175
+ }
176
+ // MCP uses callback properties, not DOM EventTarget listeners.
177
+ // oxlint-disable-next-line unicorn/prefer-add-event-listener
178
+ server.onclose = () => {
179
+ void close().catch(reportFailure);
180
+ };
181
+ // oxlint-disable-next-line unicorn/prefer-add-event-listener
182
+ server.onerror = reportFailure;
183
+ return { server, close };
184
+ }
185
+ export async function runStdio() {
186
+ const app = createBrowserMcpServer();
187
+ const shutdown = () => {
188
+ void app.close().catch(reportFailure);
189
+ };
190
+ process.once('SIGINT', shutdown);
191
+ process.once('SIGTERM', shutdown);
192
+ process.stdin.once('end', shutdown);
193
+ try {
194
+ await app.server.connect(new StdioServerTransport());
195
+ }
196
+ catch (error) {
197
+ await app.close();
198
+ throw error;
199
+ }
200
+ }
201
+ // Canonical paths handle macOS /var vs /private/var aliases in packed installs.
202
+ if (process.argv[1] &&
203
+ realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
204
+ if (process.argv.includes('--help')) {
205
+ console.error('pi-browser-use MCP server (stdio). Configure ${PLUGIN_DATA}/config.json or PI_BROWSER_USE_CONFIG. No Pi installation is required.');
206
+ }
207
+ else if (process.argv.length > 2) {
208
+ console.error('Unknown argument. Use --help; browser configuration belongs in config.json.');
209
+ process.exitCode = 1;
210
+ }
211
+ else {
212
+ runStdio().catch((error) => {
213
+ console.error(`[pi-browser-use] ${error instanceof Error ? error.message : 'MCP server failed.'}`);
214
+ process.exitCode = 1;
215
+ });
216
+ }
217
+ }
218
+ //# sourceMappingURL=mcp-server.js.map
@@ -89,7 +89,7 @@ export declare class PersistentBackend {
89
89
  */
90
90
  stop(): Promise<void>;
91
91
  /** Restart into the other visibility (headless <-> headed fallback). */
92
- restart(headed: boolean): Promise<BrowserUseConfig>;
92
+ restart(headed: boolean, signal?: AbortSignal): Promise<BrowserUseConfig>;
93
93
  }
94
94
  /**
95
95
  * Persistent mode self-launches Pi-owned Chrome (§4) unless the legacy
@@ -160,6 +160,7 @@ export class PersistentBackend {
160
160
  headless: !headed,
161
161
  chromeArgs: this.options.config.chromeArgs,
162
162
  executablePath: this.options.config.executablePath,
163
+ signal,
163
164
  });
164
165
  }
165
166
  catch (error) {
@@ -198,9 +199,14 @@ export class PersistentBackend {
198
199
  */
199
200
  attachConfig() {
200
201
  const browserUrl = this.effectiveBrowserUrl();
201
- const { userDataDir: _userDataDir, isolated: _isolated, ...rest } = this.options.config;
202
+ const { userDataDir: _userDataDir, isolated: _isolated, executablePath: _executablePath, channel: _channel, chromeArgs: _chromeArgs, ...rest } = this.options.config;
202
203
  void _userDataDir;
203
204
  void _isolated;
205
+ void _executablePath;
206
+ void _channel;
207
+ void _chromeArgs;
208
+ // We already launched Chrome with these options. Upstream rejects an
209
+ // executable/channel combined with browserUrl; attach must not relaunch.
204
210
  return { ...rest, browserUrl, isolated: false };
205
211
  }
206
212
  running() {
@@ -257,14 +263,14 @@ export class PersistentBackend {
257
263
  }
258
264
  }
259
265
  /** Restart into the other visibility (headless <-> headed fallback). */
260
- async restart(headed) {
266
+ async restart(headed, signal) {
261
267
  if (this.sharedAdvert) {
262
268
  throw new Error('Cannot restart a shared peer backend (owned by another live session). ' +
263
269
  'Ask that session to switch visibility, or wait for it to exit.');
264
270
  }
265
271
  await this.stop();
266
272
  this.options.headed = headed;
267
- return this.start();
273
+ return this.start(signal);
268
274
  }
269
275
  }
270
276
  /**
@@ -274,7 +280,6 @@ export class PersistentBackend {
274
280
  export function shouldSelfLaunch(config) {
275
281
  if (process.env['PI_BROWSER_USE_LEGACY_PERSISTENT'] === '1')
276
282
  return false;
277
- void config;
278
- return true;
283
+ return !config.browserUrl && !config.wsEndpoint && !config.autoConnect;
279
284
  }
280
285
  //# sourceMappingURL=persistent-backend.js.map
@@ -102,7 +102,7 @@ export function acquireProfileLock(profileDir, options) {
102
102
  const holderPid = readLockPid(lockPath);
103
103
  const ageMs = readLockAgeMs(lockPath);
104
104
  const holderAlive = holderPid !== undefined && isPidAlive(holderPid);
105
- const isStale = !holderAlive || (ageMs !== undefined && ageMs > staleMs);
105
+ const isStale = !holderAlive && (holderPid !== undefined || (ageMs !== undefined && ageMs > staleMs));
106
106
  if (isStale) {
107
107
  try {
108
108
  rmSync(lockPath, { force: true });
@@ -0,0 +1,45 @@
1
+ import { DevToolsClient } from './client.js';
2
+ import { type BrowserUseConfig } from './config.js';
3
+ import { PersistentBackend, type PersistentBackendOptions } from './persistent-backend.js';
4
+ import { handleAnalyzeScreenshot } from './vision.js';
5
+ type UpstreamResult = {
6
+ content?: Array<{
7
+ type: string;
8
+ text?: string;
9
+ data?: string;
10
+ mimeType?: string;
11
+ }>;
12
+ isError?: boolean;
13
+ };
14
+ /** Host-independent tool contract; Pi and MCP adapt only at their boundaries. */
15
+ export type VisionCaller = Parameters<typeof handleAnalyzeScreenshot>[1];
16
+ export interface BrowserToolContext {
17
+ callVision?: VisionCaller;
18
+ }
19
+ export interface BrowserToolDefinition {
20
+ name: string;
21
+ label: string;
22
+ description: string;
23
+ parameters: unknown;
24
+ execute: (params: Record<string, unknown>, signal?: AbortSignal, context?: BrowserToolContext) => Promise<UpstreamResult>;
25
+ }
26
+ type BrowserClient = Pick<DevToolsClient, 'ensureReady' | 'listAllTools' | 'callTool' | 'close'>;
27
+ type BrowserBackend = Pick<PersistentBackend, 'start' | 'stop' | 'running' | 'pid' | 'owned' | 'profileDir' | 'restart' | 'attachConfig'>;
28
+ export interface BrowserRuntimeOptions {
29
+ config?: BrowserUseConfig;
30
+ defaultProfileDir?: string;
31
+ artifactDir?: string;
32
+ visionEnabled?: boolean;
33
+ /** Discover tools without launching Chrome. Native Pi keeps eager startup. */
34
+ lazyBrowser?: boolean;
35
+ createClient?: (config: BrowserUseConfig) => BrowserClient;
36
+ createBackend?: (options: PersistentBackendOptions) => BrowserBackend;
37
+ }
38
+ /** Shared browser lifecycle, policy, tools, and ownership. No Pi host is required. */
39
+ export declare function createBrowserRuntime(options?: BrowserRuntimeOptions): {
40
+ start: () => Promise<BrowserToolDefinition[]>;
41
+ stop: () => Promise<void>;
42
+ };
43
+ export type BrowserRuntime = ReturnType<typeof createBrowserRuntime>;
44
+ export {};
45
+ //# sourceMappingURL=runtime.d.ts.map