@flrande/browserctl 0.5.0-dev.22.1 → 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.
- package/dist/client.d.ts +34 -0
- package/dist/client.js +138 -0
- package/dist/commandRegistry.d.ts +16 -0
- package/dist/commandRegistry.js +21 -0
- package/dist/help.d.ts +4 -0
- package/dist/help.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -0
- package/dist/runCli.d.ts +5 -0
- package/dist/runCli.js +170 -0
- package/package.json +32 -59
- package/INSTALL-CN.md +0 -92
- package/INSTALL.md +0 -92
- package/LICENSE +0 -21
- package/README-CN.md +0 -69
- package/README.md +0 -69
- package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
- package/apps/browserctl/src/commands/act.test.ts +0 -71
- package/apps/browserctl/src/commands/act.ts +0 -64
- package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
- package/apps/browserctl/src/commands/common.test.ts +0 -87
- package/apps/browserctl/src/commands/common.ts +0 -191
- package/apps/browserctl/src/commands/console-list.test.ts +0 -102
- package/apps/browserctl/src/commands/console-list.ts +0 -108
- package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
- package/apps/browserctl/src/commands/cookie-get.ts +0 -18
- package/apps/browserctl/src/commands/cookie-set.ts +0 -22
- package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
- package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
- package/apps/browserctl/src/commands/dom-query.ts +0 -18
- package/apps/browserctl/src/commands/download-trigger.ts +0 -22
- package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
- package/apps/browserctl/src/commands/download-wait.ts +0 -27
- package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
- package/apps/browserctl/src/commands/frame-list.ts +0 -16
- package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
- package/apps/browserctl/src/commands/har-export.test.ts +0 -112
- package/apps/browserctl/src/commands/har-export.ts +0 -120
- package/apps/browserctl/src/commands/memory-delete.ts +0 -20
- package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
- package/apps/browserctl/src/commands/memory-list.ts +0 -90
- package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
- package/apps/browserctl/src/commands/memory-purge.ts +0 -16
- package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
- package/apps/browserctl/src/commands/memory-status.ts +0 -16
- package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
- package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
- package/apps/browserctl/src/commands/network-list.test.ts +0 -110
- package/apps/browserctl/src/commands/network-list.ts +0 -112
- package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
- package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
- package/apps/browserctl/src/commands/profile-list.ts +0 -16
- package/apps/browserctl/src/commands/profile-use.ts +0 -18
- package/apps/browserctl/src/commands/response-body.ts +0 -24
- package/apps/browserctl/src/commands/screenshot.ts +0 -16
- package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
- package/apps/browserctl/src/commands/session-drop.ts +0 -16
- package/apps/browserctl/src/commands/session-list.test.ts +0 -81
- package/apps/browserctl/src/commands/session-list.ts +0 -70
- package/apps/browserctl/src/commands/snapshot.ts +0 -16
- package/apps/browserctl/src/commands/status.ts +0 -10
- package/apps/browserctl/src/commands/storage-get.ts +0 -20
- package/apps/browserctl/src/commands/storage-set.ts +0 -22
- package/apps/browserctl/src/commands/tab-close.ts +0 -20
- package/apps/browserctl/src/commands/tab-focus.ts +0 -20
- package/apps/browserctl/src/commands/tab-open.ts +0 -19
- package/apps/browserctl/src/commands/tabs.ts +0 -13
- package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
- package/apps/browserctl/src/commands/trace-get.ts +0 -62
- package/apps/browserctl/src/commands/upload-arm.ts +0 -26
- package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-element.ts +0 -76
- package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
- package/apps/browserctl/src/commands/wait-text.ts +0 -93
- package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
- package/apps/browserctl/src/commands/wait-url.ts +0 -76
- package/apps/browserctl/src/daemon-client.test.ts +0 -512
- package/apps/browserctl/src/daemon-client.ts +0 -632
- package/apps/browserctl/src/e2e.test.ts +0 -103
- package/apps/browserctl/src/main.dispatch.test.ts +0 -461
- package/apps/browserctl/src/main.test.ts +0 -334
- package/apps/browserctl/src/main.ts +0 -957
- package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
- package/apps/browserctl/src/test-port.ts +0 -26
- package/apps/browserd/src/bootstrap.ts +0 -432
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
- package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
- package/apps/browserd/src/container.ts +0 -3088
- package/apps/browserd/src/main.test.ts +0 -1522
- package/apps/browserd/src/main.ts +0 -7
- package/apps/browserd/src/test-port.ts +0 -26
- package/apps/browserd/src/tool-matrix.test.ts +0 -887
- package/bin/browserctl.cjs +0 -21
- package/bin/browserd.cjs +0 -21
- package/extensions/chrome-relay/README-CN.md +0 -39
- package/extensions/chrome-relay/README.md +0 -39
- package/extensions/chrome-relay/background.js +0 -1687
- package/extensions/chrome-relay/manifest.json +0 -15
- package/extensions/chrome-relay/popup.html +0 -369
- package/extensions/chrome-relay/popup.js +0 -972
- package/packages/core/src/bootstrap.test.ts +0 -10
- package/packages/core/src/driver-registry.test.ts +0 -45
- package/packages/core/src/driver-registry.ts +0 -22
- package/packages/core/src/driver.ts +0 -47
- package/packages/core/src/index.ts +0 -6
- package/packages/core/src/navigation-memory.test.ts +0 -259
- package/packages/core/src/navigation-memory.ts +0 -360
- package/packages/core/src/ref-cache.test.ts +0 -61
- package/packages/core/src/ref-cache.ts +0 -28
- package/packages/core/src/session-store.test.ts +0 -82
- package/packages/core/src/session-store.ts +0 -138
- package/packages/core/src/types.ts +0 -9
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
- package/packages/driver-chrome-relay/src/index.ts +0 -26
- package/packages/driver-managed/src/index.ts +0 -22
- package/packages/driver-managed/src/managed-driver.test.ts +0 -183
- package/packages/driver-managed/src/managed-driver.ts +0 -341
- package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
- package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
- package/packages/driver-remote-cdp/src/index.ts +0 -19
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
- package/packages/protocol/src/envelope.test.ts +0 -25
- package/packages/protocol/src/envelope.ts +0 -31
- package/packages/protocol/src/errors.test.ts +0 -17
- package/packages/protocol/src/errors.ts +0 -11
- package/packages/protocol/src/index.ts +0 -3
- package/packages/protocol/src/tools.ts +0 -3
- package/packages/transport-mcp-stdio/src/index.ts +0 -3
- package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
- package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
- package/packages/transport-mcp-stdio/src/server.ts +0 -183
- 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
|
-
}
|