@flrande/browserctl 0.5.0 → 0.6.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.
Files changed (136) hide show
  1. package/dist/client.d.ts +34 -0
  2. package/dist/client.js +138 -0
  3. package/dist/commandRegistry.d.ts +16 -0
  4. package/dist/commandRegistry.js +21 -0
  5. package/dist/help.d.ts +4 -0
  6. package/dist/help.js +24 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +23 -0
  9. package/dist/runCli.d.ts +5 -0
  10. package/dist/runCli.js +170 -0
  11. package/package.json +32 -57
  12. package/INSTALL-CN.md +0 -92
  13. package/INSTALL.md +0 -92
  14. package/LICENSE +0 -21
  15. package/README-CN.md +0 -69
  16. package/README.md +0 -69
  17. package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
  18. package/apps/browserctl/src/commands/act.test.ts +0 -71
  19. package/apps/browserctl/src/commands/act.ts +0 -64
  20. package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
  21. package/apps/browserctl/src/commands/common.test.ts +0 -87
  22. package/apps/browserctl/src/commands/common.ts +0 -191
  23. package/apps/browserctl/src/commands/console-list.test.ts +0 -102
  24. package/apps/browserctl/src/commands/console-list.ts +0 -108
  25. package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
  26. package/apps/browserctl/src/commands/cookie-get.ts +0 -18
  27. package/apps/browserctl/src/commands/cookie-set.ts +0 -22
  28. package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
  29. package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
  30. package/apps/browserctl/src/commands/dom-query.ts +0 -18
  31. package/apps/browserctl/src/commands/download-trigger.ts +0 -22
  32. package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
  33. package/apps/browserctl/src/commands/download-wait.ts +0 -27
  34. package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
  35. package/apps/browserctl/src/commands/frame-list.ts +0 -16
  36. package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
  37. package/apps/browserctl/src/commands/har-export.test.ts +0 -112
  38. package/apps/browserctl/src/commands/har-export.ts +0 -120
  39. package/apps/browserctl/src/commands/memory-delete.ts +0 -20
  40. package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
  41. package/apps/browserctl/src/commands/memory-list.ts +0 -90
  42. package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
  43. package/apps/browserctl/src/commands/memory-purge.ts +0 -16
  44. package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
  45. package/apps/browserctl/src/commands/memory-status.ts +0 -16
  46. package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
  47. package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
  48. package/apps/browserctl/src/commands/network-list.test.ts +0 -110
  49. package/apps/browserctl/src/commands/network-list.ts +0 -112
  50. package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
  51. package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
  52. package/apps/browserctl/src/commands/profile-list.ts +0 -16
  53. package/apps/browserctl/src/commands/profile-use.ts +0 -18
  54. package/apps/browserctl/src/commands/response-body.ts +0 -24
  55. package/apps/browserctl/src/commands/screenshot.ts +0 -16
  56. package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
  57. package/apps/browserctl/src/commands/session-drop.ts +0 -16
  58. package/apps/browserctl/src/commands/session-list.test.ts +0 -81
  59. package/apps/browserctl/src/commands/session-list.ts +0 -70
  60. package/apps/browserctl/src/commands/snapshot.ts +0 -16
  61. package/apps/browserctl/src/commands/status.ts +0 -10
  62. package/apps/browserctl/src/commands/storage-get.ts +0 -20
  63. package/apps/browserctl/src/commands/storage-set.ts +0 -22
  64. package/apps/browserctl/src/commands/tab-close.ts +0 -20
  65. package/apps/browserctl/src/commands/tab-focus.ts +0 -20
  66. package/apps/browserctl/src/commands/tab-open.ts +0 -19
  67. package/apps/browserctl/src/commands/tabs.ts +0 -13
  68. package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
  69. package/apps/browserctl/src/commands/trace-get.ts +0 -62
  70. package/apps/browserctl/src/commands/upload-arm.ts +0 -26
  71. package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
  72. package/apps/browserctl/src/commands/wait-element.ts +0 -76
  73. package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
  74. package/apps/browserctl/src/commands/wait-text.ts +0 -93
  75. package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
  76. package/apps/browserctl/src/commands/wait-url.ts +0 -76
  77. package/apps/browserctl/src/daemon-client.test.ts +0 -253
  78. package/apps/browserctl/src/daemon-client.ts +0 -632
  79. package/apps/browserctl/src/e2e.test.ts +0 -103
  80. package/apps/browserctl/src/main.dispatch.test.ts +0 -461
  81. package/apps/browserctl/src/main.test.ts +0 -334
  82. package/apps/browserctl/src/main.ts +0 -957
  83. package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
  84. package/apps/browserctl/src/test-port.ts +0 -26
  85. package/apps/browserd/src/bootstrap.ts +0 -432
  86. package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
  87. package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
  88. package/apps/browserd/src/container.ts +0 -3088
  89. package/apps/browserd/src/main.test.ts +0 -1436
  90. package/apps/browserd/src/main.ts +0 -7
  91. package/apps/browserd/src/test-port.ts +0 -26
  92. package/apps/browserd/src/tool-matrix.test.ts +0 -887
  93. package/bin/browserctl.cjs +0 -21
  94. package/bin/browserd.cjs +0 -21
  95. package/extensions/chrome-relay/README-CN.md +0 -39
  96. package/extensions/chrome-relay/README.md +0 -39
  97. package/extensions/chrome-relay/background.js +0 -1687
  98. package/extensions/chrome-relay/manifest.json +0 -15
  99. package/extensions/chrome-relay/popup.html +0 -369
  100. package/extensions/chrome-relay/popup.js +0 -972
  101. package/packages/core/src/bootstrap.test.ts +0 -10
  102. package/packages/core/src/driver-registry.test.ts +0 -45
  103. package/packages/core/src/driver-registry.ts +0 -22
  104. package/packages/core/src/driver.ts +0 -47
  105. package/packages/core/src/index.ts +0 -6
  106. package/packages/core/src/navigation-memory.test.ts +0 -259
  107. package/packages/core/src/navigation-memory.ts +0 -360
  108. package/packages/core/src/ref-cache.test.ts +0 -61
  109. package/packages/core/src/ref-cache.ts +0 -28
  110. package/packages/core/src/session-store.test.ts +0 -82
  111. package/packages/core/src/session-store.ts +0 -138
  112. package/packages/core/src/types.ts +0 -9
  113. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
  114. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
  115. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
  116. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
  117. package/packages/driver-chrome-relay/src/index.ts +0 -26
  118. package/packages/driver-managed/src/index.ts +0 -22
  119. package/packages/driver-managed/src/managed-driver.test.ts +0 -183
  120. package/packages/driver-managed/src/managed-driver.ts +0 -341
  121. package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
  122. package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
  123. package/packages/driver-remote-cdp/src/index.ts +0 -19
  124. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
  125. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
  126. package/packages/protocol/src/envelope.test.ts +0 -25
  127. package/packages/protocol/src/envelope.ts +0 -31
  128. package/packages/protocol/src/errors.test.ts +0 -17
  129. package/packages/protocol/src/errors.ts +0 -11
  130. package/packages/protocol/src/index.ts +0 -3
  131. package/packages/protocol/src/tools.ts +0 -3
  132. package/packages/transport-mcp-stdio/src/index.ts +0 -3
  133. package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
  134. package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
  135. package/packages/transport-mcp-stdio/src/server.ts +0 -183
  136. package/packages/transport-mcp-stdio/src/tool-map.ts +0 -84
@@ -1,97 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
-
3
- import { stopDaemon } from "./daemon-client";
4
- import { EXIT_CODES, runCli } from "./main";
5
- import { reserveLoopbackPort } from "./test-port";
6
-
7
- let testDaemonPort = 0;
8
- let activeRelayPort = 0;
9
-
10
- function createIoCapture() {
11
- const state = {
12
- stdout: "",
13
- stderr: ""
14
- };
15
-
16
- return {
17
- state,
18
- io: {
19
- stdout: {
20
- write(content: string) {
21
- state.stdout += content;
22
- }
23
- },
24
- stderr: {
25
- write(content: string) {
26
- state.stderr += content;
27
- }
28
- }
29
- }
30
- };
31
- }
32
-
33
- function parseJsonLine(state: { stdout: string }): Record<string, unknown> {
34
- return JSON.parse(state.stdout.trim()) as Record<string, unknown>;
35
- }
36
-
37
- beforeEach(async () => {
38
- testDaemonPort = await reserveLoopbackPort();
39
- activeRelayPort = await reserveLoopbackPort();
40
- while (activeRelayPort === testDaemonPort) {
41
- activeRelayPort = await reserveLoopbackPort();
42
- }
43
-
44
- process.env.BROWSERCTL_DAEMON_PORT = String(testDaemonPort);
45
- process.env.BROWSERD_CHROME_RELAY_URL = `http://127.0.0.1:${activeRelayPort}`;
46
- delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
47
- delete process.env.BROWSERD_DEFAULT_DRIVER;
48
- delete process.env.BROWSERD_CHROME_RELAY_MODE;
49
- delete process.env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN;
50
- await stopDaemon(testDaemonPort);
51
- });
52
-
53
- afterEach(async () => {
54
- await stopDaemon(testDaemonPort);
55
- delete process.env.BROWSERCTL_DAEMON_PORT;
56
- delete process.env.BROWSERD_CHROME_RELAY_URL;
57
- delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
58
- delete process.env.BROWSERD_DEFAULT_DRIVER;
59
- delete process.env.BROWSERD_CHROME_RELAY_MODE;
60
- delete process.env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN;
61
- });
62
-
63
- describe("browserctl smoke e2e", () => {
64
- it("starts with extension-first defaults and reports chrome-relay status", async () => {
65
- const startCapture = createIoCapture();
66
- const startExitCode = await runCli(["daemon-start", "--json"], startCapture.io);
67
-
68
- expect(startExitCode).toBe(EXIT_CODES.OK);
69
- const startPayload = parseJsonLine(startCapture.state);
70
- expect(startPayload.ok).toBe(true);
71
-
72
- const statusCapture = createIoCapture();
73
- const statusExitCode = await runCli(["status", "--json"], statusCapture.io);
74
-
75
- expect(statusExitCode).toBe(EXIT_CODES.OK);
76
- const statusPayload = parseJsonLine(statusCapture.state);
77
- expect(statusPayload.ok).toBe(true);
78
- const statusData = statusPayload.data as Record<string, unknown>;
79
- expect(statusData.driver).toBe("chrome-relay");
80
- expect(statusData.status).toMatchObject({
81
- kind: "chrome-relay",
82
- connected: false
83
- });
84
- const relayStatus = statusData.status as Record<string, unknown>;
85
- expect(String(relayStatus.relayUrl ?? "")).toContain(`:${activeRelayPort}`);
86
-
87
- const stopCapture = createIoCapture();
88
- const stopExitCode = await runCli(["daemon-stop", "--json"], stopCapture.io);
89
- expect(stopExitCode).toBe(EXIT_CODES.OK);
90
- const stopPayload = parseJsonLine(stopCapture.state);
91
- expect(stopPayload.ok).toBe(true);
92
- expect(stopPayload.data).toMatchObject({
93
- stopped: true,
94
- port: testDaemonPort
95
- });
96
- }, 20_000);
97
- });
@@ -1,26 +0,0 @@
1
- import { createServer } from "node:net";
2
-
3
- export async function reserveLoopbackPort(): Promise<number> {
4
- return await new Promise<number>((resolve, reject) => {
5
- const server = createServer();
6
-
7
- server.once("error", reject);
8
- server.listen(0, "127.0.0.1", () => {
9
- const address = server.address();
10
- if (typeof address !== "object" || address === null) {
11
- server.close(() => reject(new Error("Failed to reserve loopback port.")));
12
- return;
13
- }
14
-
15
- const port = address.port;
16
- server.close((error) => {
17
- if (error !== undefined) {
18
- reject(error);
19
- return;
20
- }
21
-
22
- resolve(port);
23
- });
24
- });
25
- });
26
- }
@@ -1,432 +0,0 @@
1
- import { createContainer, loadBrowserdConfig, type BrowserdContainer } from "./container";
2
- import { createMcpSdkServer } from "../../../packages/transport-mcp-stdio/src";
3
- import { createServer, Socket } from "node:net";
4
- import { Readable, Writable } from "node:stream";
5
-
6
- const UNKNOWN_TRACE_ID = "trace:unknown";
7
- const UNKNOWN_SESSION_ID = "session:unknown";
8
- const INVALID_REQUEST_CODE = "E_INVALID_ARG";
9
- const DEFAULT_TCP_HOST = "127.0.0.1";
10
- const DEFAULT_TCP_PORT = 41337;
11
- const DEFAULT_SERVER_VERSION = "0.1.0";
12
-
13
- type StdioToolRequest = {
14
- name: string;
15
- arguments?: unknown;
16
- traceId?: string;
17
- id?: unknown;
18
- };
19
-
20
- type StdioTransport = {
21
- input: Readable;
22
- output: Writable;
23
- };
24
-
25
- type StdioProtocol = "mcp" | "legacy";
26
-
27
- type TcpTransport = {
28
- host: string;
29
- port: number;
30
- };
31
-
32
- type ErrorEnvelope = {
33
- id?: unknown;
34
- ok: false;
35
- traceId: string;
36
- sessionId: string;
37
- error: {
38
- code: string;
39
- message: string;
40
- };
41
- };
42
-
43
- type RequestMetadata = {
44
- id?: unknown;
45
- traceId?: string;
46
- sessionId?: string;
47
- };
48
-
49
- class ProcessLineError extends Error {
50
- readonly metadata: RequestMetadata;
51
-
52
- constructor(message: string, metadata: RequestMetadata) {
53
- super(message);
54
- this.metadata = metadata;
55
- }
56
- }
57
-
58
- function resolveNonEmptyString(value: unknown): string | undefined {
59
- if (typeof value !== "string") {
60
- return undefined;
61
- }
62
-
63
- const trimmedValue = value.trim();
64
- return trimmedValue.length === 0 ? undefined : trimmedValue;
65
- }
66
-
67
- function extractRequestMetadata(value: unknown): RequestMetadata {
68
- if (!isObjectRecord(value)) {
69
- return {};
70
- }
71
-
72
- const metadata: RequestMetadata = {};
73
- if (Object.prototype.hasOwnProperty.call(value, "id")) {
74
- metadata.id = value.id;
75
- }
76
-
77
- const traceId = resolveNonEmptyString(value.traceId);
78
- if (traceId !== undefined) {
79
- metadata.traceId = traceId;
80
- }
81
-
82
- const args = value.arguments;
83
- if (isObjectRecord(args)) {
84
- const sessionId = resolveNonEmptyString(args.sessionId);
85
- if (sessionId !== undefined) {
86
- metadata.sessionId = sessionId;
87
- }
88
- }
89
-
90
- return metadata;
91
- }
92
-
93
- function toErrorMessage(error: unknown): string {
94
- return error instanceof Error ? error.message : "Unexpected request handling failure.";
95
- }
96
-
97
- function createInvalidRequestEnvelope(
98
- message: string,
99
- metadata: RequestMetadata = {}
100
- ): ErrorEnvelope {
101
- const envelope: ErrorEnvelope = {
102
- ok: false,
103
- traceId: metadata.traceId ?? UNKNOWN_TRACE_ID,
104
- sessionId: metadata.sessionId ?? UNKNOWN_SESSION_ID,
105
- error: {
106
- code: INVALID_REQUEST_CODE,
107
- message
108
- }
109
- };
110
-
111
- if (metadata.id !== undefined) {
112
- envelope.id = metadata.id;
113
- }
114
-
115
- return envelope;
116
- }
117
-
118
- function isObjectRecord(value: unknown): value is Record<string, unknown> {
119
- return typeof value === "object" && value !== null && !Array.isArray(value);
120
- }
121
-
122
- function normalizeToolRequest(value: unknown): ToolRequestParseResult {
123
- if (!isObjectRecord(value)) {
124
- return {
125
- ok: false,
126
- error: createInvalidRequestEnvelope("Request must be a JSON object.")
127
- };
128
- }
129
-
130
- const metadata = extractRequestMetadata(value);
131
- const { id, name, arguments: args, traceId } = value;
132
- if (typeof name !== "string" || name.trim().length === 0) {
133
- return {
134
- ok: false,
135
- error: createInvalidRequestEnvelope(
136
- 'Request field "name" must be a non-empty string.',
137
- metadata
138
- )
139
- };
140
- }
141
-
142
- const request: StdioToolRequest = {
143
- name: name.trim(),
144
- arguments: args
145
- };
146
-
147
- if (typeof traceId === "string") {
148
- request.traceId = traceId;
149
- }
150
-
151
- if (Object.prototype.hasOwnProperty.call(value, "id")) {
152
- request.id = id;
153
- }
154
-
155
- return {
156
- ok: true,
157
- request
158
- };
159
- }
160
-
161
- type ToolRequestParseResult =
162
- | {
163
- ok: true;
164
- request: StdioToolRequest;
165
- }
166
- | {
167
- ok: false;
168
- error: ErrorEnvelope;
169
- };
170
-
171
- function writeJsonLine(output: Writable, payload: unknown): void {
172
- output.write(`${JSON.stringify(payload)}\n`);
173
- }
174
-
175
- export type BrowserdRuntime = {
176
- container: BrowserdContainer;
177
- transport: "stdio" | "tcp";
178
- listening?: TcpTransport;
179
- mcpStdioStarted: true;
180
- close(): void;
181
- };
182
-
183
- type LineWriter = (payload: unknown) => void;
184
-
185
- function createLineProcessor(container: BrowserdContainer, writer: LineWriter) {
186
- return async (line: string): Promise<void> => {
187
- const trimmedLine = line.trim();
188
- if (trimmedLine.length === 0) {
189
- return;
190
- }
191
-
192
- let parsedRequest: unknown;
193
- try {
194
- parsedRequest = JSON.parse(trimmedLine);
195
- } catch {
196
- writer(createInvalidRequestEnvelope("Invalid JSON request."));
197
- return;
198
- }
199
-
200
- const requestMetadata = extractRequestMetadata(parsedRequest);
201
- try {
202
- const normalizedRequest = normalizeToolRequest(parsedRequest);
203
- if (!normalizedRequest.ok) {
204
- writer(normalizedRequest.error);
205
- return;
206
- }
207
-
208
- const request = normalizedRequest.request;
209
- const response = await container.mcpServer.callTool({
210
- name: request.name,
211
- arguments: request.arguments,
212
- traceId: request.traceId
213
- });
214
-
215
- const payload = request.id === undefined ? response : { id: request.id, ...response };
216
- writer(payload);
217
- } catch (error) {
218
- throw new ProcessLineError(toErrorMessage(error), requestMetadata);
219
- }
220
- };
221
- }
222
-
223
- function createLineQueue(
224
- processLine: (line: string) => Promise<void>,
225
- writer: LineWriter
226
- ): (line: string) => void {
227
- let processing = Promise.resolve();
228
-
229
- return (line: string) => {
230
- processing = processing
231
- .then(() => processLine(line))
232
- .catch((error: unknown) => {
233
- if (error instanceof ProcessLineError) {
234
- writer(createInvalidRequestEnvelope(error.message, error.metadata));
235
- return;
236
- }
237
-
238
- writer(createInvalidRequestEnvelope(toErrorMessage(error)));
239
- });
240
- };
241
- }
242
-
243
- function attachStreamReader(
244
- input: Readable,
245
- onLine: (line: string) => void
246
- ): () => void {
247
- let buffer = "";
248
- const onData = (chunk: string | Buffer) => {
249
- buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
250
-
251
- let lineBreakIndex = buffer.indexOf("\n");
252
- while (lineBreakIndex >= 0) {
253
- const line = buffer.slice(0, lineBreakIndex);
254
- buffer = buffer.slice(lineBreakIndex + 1);
255
- onLine(line);
256
- lineBreakIndex = buffer.indexOf("\n");
257
- }
258
- };
259
-
260
- input.setEncoding("utf8");
261
- input.on("data", onData);
262
- input.resume();
263
- return () => {
264
- input.off("data", onData);
265
- input.pause();
266
- };
267
- }
268
-
269
- function toTcpTransport(value: BootstrapBrowserdOptions): TcpTransport {
270
- const env = value.env ?? process.env;
271
- const envPort = toPositiveNumber(env.BROWSERD_PORT);
272
- const optionPort = value.port;
273
- return {
274
- host: value.host ?? env.BROWSERD_HOST ?? DEFAULT_TCP_HOST,
275
- port: optionPort ?? envPort ?? DEFAULT_TCP_PORT
276
- };
277
- }
278
-
279
- function toPositiveNumber(value: string | undefined): number | undefined {
280
- if (value === undefined) {
281
- return undefined;
282
- }
283
-
284
- const parsed = Number.parseInt(value, 10);
285
- return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
286
- }
287
-
288
- function resolveTransportMode(options: BootstrapBrowserdOptions): "stdio" | "tcp" {
289
- if (options.transport !== undefined) {
290
- return options.transport;
291
- }
292
-
293
- const env = options.env ?? process.env;
294
- return env.BROWSERD_TRANSPORT === "tcp" ? "tcp" : "stdio";
295
- }
296
-
297
- export function startMcpStdioServer(
298
- container: BrowserdContainer,
299
- transport: StdioTransport = { input: process.stdin, output: process.stdout }
300
- ): BrowserdRuntime {
301
- const mcpSdkServer = createMcpSdkServer({
302
- toolMap: container.mcpServer.toolMap,
303
- serverInfo: {
304
- name: "browserd",
305
- version: DEFAULT_SERVER_VERSION
306
- }
307
- });
308
-
309
- // MCP server startup is async but starts listening synchronously before this Promise resolves.
310
- void mcpSdkServer.connectStdio(transport).catch(() => undefined);
311
-
312
- return {
313
- container,
314
- transport: "stdio",
315
- mcpStdioStarted: true,
316
- close() {
317
- void mcpSdkServer.close();
318
- container.close();
319
- }
320
- };
321
- }
322
-
323
- export function startLegacyStdioServer(
324
- container: BrowserdContainer,
325
- transport: StdioTransport = { input: process.stdin, output: process.stdout }
326
- ): BrowserdRuntime {
327
- const writer: LineWriter = (payload) => {
328
- writeJsonLine(transport.output, payload);
329
- };
330
- const processLine = createLineProcessor(container, writer);
331
- const queueLine = createLineQueue(processLine, writer);
332
- const detach = attachStreamReader(transport.input, queueLine);
333
-
334
- return {
335
- container,
336
- transport: "stdio",
337
- mcpStdioStarted: true,
338
- close() {
339
- detach();
340
- container.close();
341
- }
342
- };
343
- }
344
-
345
- export function startMcpTcpServer(container: BrowserdContainer, transport: TcpTransport): BrowserdRuntime {
346
- const server = createServer();
347
- const sockets = new Set<Socket>();
348
-
349
- server.on("connection", (socket) => {
350
- sockets.add(socket);
351
- socket.setEncoding("utf8");
352
-
353
- const writer: LineWriter = (payload) => {
354
- socket.write(`${JSON.stringify(payload)}\n`);
355
- };
356
-
357
- const processLine = createLineProcessor(container, writer);
358
- const queueLine = createLineQueue(processLine, writer);
359
- const detachReader = attachStreamReader(socket, queueLine);
360
-
361
- socket.on("close", () => {
362
- detachReader();
363
- sockets.delete(socket);
364
- });
365
-
366
- socket.on("error", () => {
367
- detachReader();
368
- sockets.delete(socket);
369
- });
370
- });
371
-
372
- server.listen(transport.port, transport.host);
373
-
374
- return {
375
- container,
376
- transport: "tcp",
377
- listening: transport,
378
- mcpStdioStarted: true,
379
- close() {
380
- for (const socket of sockets) {
381
- socket.destroy();
382
- }
383
- sockets.clear();
384
- server.close();
385
- container.close();
386
- }
387
- };
388
- }
389
-
390
- type BootstrapBrowserdOptions = {
391
- env?: Record<string, string | undefined>;
392
- transport?: "stdio" | "tcp";
393
- stdioProtocol?: StdioProtocol;
394
- host?: string;
395
- port?: number;
396
- input?: Readable;
397
- output?: Writable;
398
- };
399
-
400
- function resolveStdioProtocol(options: BootstrapBrowserdOptions): StdioProtocol {
401
- if (options.stdioProtocol !== undefined) {
402
- return options.stdioProtocol;
403
- }
404
-
405
- const env = options.env ?? process.env;
406
- return env.BROWSERD_STDIO_PROTOCOL === "legacy" ? "legacy" : "mcp";
407
- }
408
-
409
- export function bootstrapBrowserd(options: BootstrapBrowserdOptions = {}): BrowserdRuntime {
410
- const config = loadBrowserdConfig(options.env);
411
- const container = createContainer(config);
412
- const mode = resolveTransportMode(options);
413
- if (mode === "tcp") {
414
- if (config.authToken === undefined) {
415
- container.close();
416
- throw new Error(
417
- "BROWSERD_AUTH_TOKEN is required when BROWSERD_TRANSPORT=tcp to prevent unauthenticated network access."
418
- );
419
- }
420
- return startMcpTcpServer(container, toTcpTransport(options));
421
- }
422
-
423
- const stdioTransport: StdioTransport = {
424
- input: options.input ?? process.stdin,
425
- output: options.output ?? process.stdout
426
- };
427
- if (resolveStdioProtocol(options) === "legacy") {
428
- return startLegacyStdioServer(container, stdioTransport);
429
- }
430
-
431
- return startMcpStdioServer(container, stdioTransport);
432
- }