@_nazmiforreal/agent-browser-mcp 0.2.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/.mcp.json +12 -0
- package/AGENTS.md +100 -0
- package/README.md +502 -0
- package/dist/browsers/discover.d.ts +13 -0
- package/dist/browsers/discover.d.ts.map +1 -0
- package/dist/browsers/discover.js +54 -0
- package/dist/browsers/discover.js.map +1 -0
- package/dist/browsers/env.d.ts +10 -0
- package/dist/browsers/env.d.ts.map +1 -0
- package/dist/browsers/env.js +72 -0
- package/dist/browsers/env.js.map +1 -0
- package/dist/browsers/index.d.ts +5 -0
- package/dist/browsers/index.d.ts.map +1 -0
- package/dist/browsers/index.js +5 -0
- package/dist/browsers/index.js.map +1 -0
- package/dist/browsers/installer.d.ts +13 -0
- package/dist/browsers/installer.d.ts.map +1 -0
- package/dist/browsers/installer.js +59 -0
- package/dist/browsers/installer.js.map +1 -0
- package/dist/browsers/registry.d.ts +14 -0
- package/dist/browsers/registry.d.ts.map +1 -0
- package/dist/browsers/registry.js +119 -0
- package/dist/browsers/registry.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +153 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/browser.d.ts +3 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +885 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/executor.d.ts +9 -0
- package/dist/tools/executor.d.ts.map +1 -0
- package/dist/tools/executor.js +1067 -0
- package/dist/tools/executor.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -0
- package/package.json +60 -0
- package/tests/browser-tools.test.ts +329 -0
- package/tests/executor.test.ts +356 -0
- package/tests/integration.test.ts +467 -0
- package/tests/server.test.ts +224 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
|
|
4
|
+
// Create mock spawn function at module level
|
|
5
|
+
const mockSpawn = vi.fn();
|
|
6
|
+
vi.mock("child_process", () => ({
|
|
7
|
+
spawn: mockSpawn,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// Helper to create mock process
|
|
11
|
+
function createMockProcess(exitCode = 0) {
|
|
12
|
+
const stdout = new EventEmitter();
|
|
13
|
+
const stderr = new EventEmitter();
|
|
14
|
+
|
|
15
|
+
const mockProc = {
|
|
16
|
+
stdout,
|
|
17
|
+
stderr,
|
|
18
|
+
on: vi.fn((event: string, callback: Function) => {
|
|
19
|
+
if (event === "close") {
|
|
20
|
+
setImmediate(() => callback(exitCode));
|
|
21
|
+
}
|
|
22
|
+
return mockProc;
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return mockProc;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("executor", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.resetModules();
|
|
32
|
+
mockSpawn.mockReset();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("execBrowser", () => {
|
|
36
|
+
it("should execute a simple command", async () => {
|
|
37
|
+
const mockProc = createMockProcess();
|
|
38
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
39
|
+
|
|
40
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
41
|
+
|
|
42
|
+
const resultPromise = execBrowser("navigate", { url: "https://example.com" });
|
|
43
|
+
mockProc.stdout.emit("data", "Navigation successful");
|
|
44
|
+
|
|
45
|
+
const result = await resultPromise;
|
|
46
|
+
|
|
47
|
+
expect(result).toBe("Navigation successful");
|
|
48
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
49
|
+
"agent-browser",
|
|
50
|
+
["open", "https://example.com"],
|
|
51
|
+
expect.any(Object)
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should include session ID when provided", async () => {
|
|
56
|
+
const mockProc = createMockProcess();
|
|
57
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
58
|
+
|
|
59
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
60
|
+
|
|
61
|
+
const resultPromise = execBrowser("click", { selector: "#btn" }, "session-123");
|
|
62
|
+
mockProc.stdout.emit("data", "Clicked");
|
|
63
|
+
|
|
64
|
+
await resultPromise;
|
|
65
|
+
|
|
66
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
67
|
+
"agent-browser",
|
|
68
|
+
expect.arrayContaining(["click", "--session", "session-123", "#btn"]),
|
|
69
|
+
expect.objectContaining({
|
|
70
|
+
env: expect.objectContaining({
|
|
71
|
+
AGENT_BROWSER_SESSION: "session-123",
|
|
72
|
+
}),
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should handle boolean options correctly", async () => {
|
|
78
|
+
const mockProc = createMockProcess();
|
|
79
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
80
|
+
|
|
81
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
82
|
+
|
|
83
|
+
const resultPromise = execBrowser("screenshot", { fullPage: true, path: "/tmp/shot.png" });
|
|
84
|
+
mockProc.stdout.emit("data", "Screenshot saved");
|
|
85
|
+
|
|
86
|
+
await resultPromise;
|
|
87
|
+
|
|
88
|
+
const callArgs = mockSpawn.mock.calls[0][1];
|
|
89
|
+
expect(callArgs).toContain("-f");
|
|
90
|
+
expect(callArgs).toContain("/tmp/shot.png");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle array options as JSON", async () => {
|
|
94
|
+
const mockProc = createMockProcess();
|
|
95
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
96
|
+
|
|
97
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
98
|
+
|
|
99
|
+
const cookies = [{ name: "session", value: "abc123" }];
|
|
100
|
+
const resultPromise = execBrowser("set_cookies", { cookies });
|
|
101
|
+
mockProc.stdout.emit("data", "Cookies set");
|
|
102
|
+
|
|
103
|
+
await resultPromise;
|
|
104
|
+
|
|
105
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
106
|
+
"agent-browser",
|
|
107
|
+
["cookies", "set", "session", "abc123"],
|
|
108
|
+
expect.any(Object)
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should handle object options as JSON", async () => {
|
|
113
|
+
const mockProc = createMockProcess();
|
|
114
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
115
|
+
|
|
116
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
117
|
+
|
|
118
|
+
const viewport = { width: 1920, height: 1080 };
|
|
119
|
+
const resultPromise = execBrowser("new_session", { viewport });
|
|
120
|
+
mockProc.stdout.emit("data", "Session created");
|
|
121
|
+
|
|
122
|
+
await resultPromise;
|
|
123
|
+
|
|
124
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
125
|
+
"agent-browser",
|
|
126
|
+
expect.arrayContaining(["session"]),
|
|
127
|
+
expect.any(Object)
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should skip null and undefined options", async () => {
|
|
132
|
+
const mockProc = createMockProcess();
|
|
133
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
134
|
+
|
|
135
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
136
|
+
|
|
137
|
+
const resultPromise = execBrowser("get_text", { selector: null, other: undefined } as any);
|
|
138
|
+
mockProc.stdout.emit("data", "Page text");
|
|
139
|
+
|
|
140
|
+
await resultPromise;
|
|
141
|
+
|
|
142
|
+
expect(mockSpawn).toHaveBeenCalledWith("agent-browser", ["get", "text"], expect.any(Object));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should convert camelCase to kebab-case", async () => {
|
|
146
|
+
const mockProc = createMockProcess();
|
|
147
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
148
|
+
|
|
149
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
150
|
+
|
|
151
|
+
const resultPromise = execBrowser("screenshot", { fullPage: true });
|
|
152
|
+
mockProc.stdout.emit("data", "Done");
|
|
153
|
+
|
|
154
|
+
await resultPromise;
|
|
155
|
+
|
|
156
|
+
const callArgs = mockSpawn.mock.calls[0][1];
|
|
157
|
+
expect(callArgs).toContain("-f");
|
|
158
|
+
expect(callArgs).not.toContain("--fullPage");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should reject on non-zero exit code", async () => {
|
|
162
|
+
const mockProc = createMockProcess(1);
|
|
163
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
164
|
+
|
|
165
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
166
|
+
|
|
167
|
+
const resultPromise = execBrowser("navigate", { url: "invalid" });
|
|
168
|
+
mockProc.stderr.emit("data", "Invalid URL");
|
|
169
|
+
|
|
170
|
+
await expect(resultPromise).rejects.toThrow("agent-browser exited with code 1");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should reject on spawn error", async () => {
|
|
174
|
+
const stdout = new EventEmitter();
|
|
175
|
+
const stderr = new EventEmitter();
|
|
176
|
+
|
|
177
|
+
const mockProc = {
|
|
178
|
+
stdout,
|
|
179
|
+
stderr,
|
|
180
|
+
on: vi.fn((event: string, callback: Function) => {
|
|
181
|
+
if (event === "error") {
|
|
182
|
+
setImmediate(() => callback(new Error("Command not found")));
|
|
183
|
+
}
|
|
184
|
+
return mockProc;
|
|
185
|
+
}),
|
|
186
|
+
};
|
|
187
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
188
|
+
|
|
189
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
190
|
+
|
|
191
|
+
const resultPromise = execBrowser("navigate", { url: "https://example.com" });
|
|
192
|
+
|
|
193
|
+
await expect(resultPromise).rejects.toThrow("Failed to execute agent-browser");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should return default message when stdout is empty", async () => {
|
|
197
|
+
const mockProc = createMockProcess();
|
|
198
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
199
|
+
|
|
200
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
201
|
+
|
|
202
|
+
const resultPromise = execBrowser("click", { selector: "#btn" });
|
|
203
|
+
// No stdout data emitted
|
|
204
|
+
|
|
205
|
+
const result = await resultPromise;
|
|
206
|
+
expect(result).toBe("Command executed successfully");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should accumulate multiple stdout chunks", async () => {
|
|
210
|
+
const mockProc = createMockProcess();
|
|
211
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
212
|
+
|
|
213
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
214
|
+
|
|
215
|
+
const resultPromise = execBrowser("get_html", {});
|
|
216
|
+
mockProc.stdout.emit("data", "<html>");
|
|
217
|
+
mockProc.stdout.emit("data", "<body>");
|
|
218
|
+
mockProc.stdout.emit("data", "</body></html>");
|
|
219
|
+
|
|
220
|
+
const result = await resultPromise;
|
|
221
|
+
expect(result).toBe("<html><body></body></html>");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should not include false boolean options", async () => {
|
|
225
|
+
const mockProc = createMockProcess();
|
|
226
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
227
|
+
|
|
228
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
229
|
+
|
|
230
|
+
const resultPromise = execBrowser("screenshot", { fullPage: false });
|
|
231
|
+
mockProc.stdout.emit("data", "Done");
|
|
232
|
+
|
|
233
|
+
await resultPromise;
|
|
234
|
+
|
|
235
|
+
const callArgs = mockSpawn.mock.calls[0][1];
|
|
236
|
+
expect(callArgs).not.toContain("-f");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("storage_get_local should include get subcommand", async () => {
|
|
240
|
+
const mockProc = createMockProcess();
|
|
241
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
242
|
+
|
|
243
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
244
|
+
|
|
245
|
+
const resultPromise = execBrowser("storage_get_local", { key: "authToken" });
|
|
246
|
+
mockProc.stdout.emit("data", "abc123");
|
|
247
|
+
|
|
248
|
+
await resultPromise;
|
|
249
|
+
|
|
250
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
251
|
+
"agent-browser",
|
|
252
|
+
["storage", "local", "get", "authToken"],
|
|
253
|
+
expect.any(Object)
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("get_attribute should put selector before attr name", async () => {
|
|
258
|
+
const mockProc = createMockProcess();
|
|
259
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
260
|
+
|
|
261
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
262
|
+
|
|
263
|
+
const resultPromise = execBrowser("get_attribute", { selector: "#link", attribute: "href" });
|
|
264
|
+
mockProc.stdout.emit("data", "https://example.com");
|
|
265
|
+
|
|
266
|
+
await resultPromise;
|
|
267
|
+
|
|
268
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
269
|
+
"agent-browser",
|
|
270
|
+
["get", "attr", "#link", "href"],
|
|
271
|
+
expect.any(Object)
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should handle numeric options", async () => {
|
|
276
|
+
const mockProc = createMockProcess();
|
|
277
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
278
|
+
|
|
279
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
280
|
+
|
|
281
|
+
const resultPromise = execBrowser("scroll", { amount: 500, direction: "down" });
|
|
282
|
+
mockProc.stdout.emit("data", "Scrolled");
|
|
283
|
+
|
|
284
|
+
await resultPromise;
|
|
285
|
+
|
|
286
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
287
|
+
"agent-browser",
|
|
288
|
+
expect.arrayContaining(["scroll", "down", "500"]),
|
|
289
|
+
expect.any(Object)
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("checkAgentBrowser", () => {
|
|
295
|
+
it("should return true when agent-browser is available", async () => {
|
|
296
|
+
const mockProc = createMockProcess();
|
|
297
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
298
|
+
|
|
299
|
+
const { checkAgentBrowser } = await import("../src/tools/executor.js");
|
|
300
|
+
|
|
301
|
+
const resultPromise = checkAgentBrowser();
|
|
302
|
+
mockProc.stdout.emit("data", "1.0.0");
|
|
303
|
+
|
|
304
|
+
const result = await resultPromise;
|
|
305
|
+
expect(result).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should return false when agent-browser fails", async () => {
|
|
309
|
+
const mockProc = createMockProcess(1);
|
|
310
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
311
|
+
|
|
312
|
+
const { checkAgentBrowser } = await import("../src/tools/executor.js");
|
|
313
|
+
|
|
314
|
+
const result = await checkAgentBrowser();
|
|
315
|
+
expect(result).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should return false when spawn error occurs", async () => {
|
|
319
|
+
const stdout = new EventEmitter();
|
|
320
|
+
const stderr = new EventEmitter();
|
|
321
|
+
|
|
322
|
+
const mockProc = {
|
|
323
|
+
stdout,
|
|
324
|
+
stderr,
|
|
325
|
+
on: vi.fn((event: string, callback: Function) => {
|
|
326
|
+
if (event === "error") {
|
|
327
|
+
setImmediate(() => callback(new Error("Command not found")));
|
|
328
|
+
}
|
|
329
|
+
return mockProc;
|
|
330
|
+
}),
|
|
331
|
+
};
|
|
332
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
333
|
+
|
|
334
|
+
const { checkAgentBrowser } = await import("../src/tools/executor.js");
|
|
335
|
+
|
|
336
|
+
const result = await checkAgentBrowser();
|
|
337
|
+
expect(result).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("environment configuration", () => {
|
|
342
|
+
it("should use default agent-browser command", async () => {
|
|
343
|
+
const mockProc = createMockProcess();
|
|
344
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
345
|
+
|
|
346
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
347
|
+
|
|
348
|
+
const resultPromise = execBrowser("navigate", { url: "https://example.com" });
|
|
349
|
+
mockProc.stdout.emit("data", "Done");
|
|
350
|
+
|
|
351
|
+
await resultPromise;
|
|
352
|
+
|
|
353
|
+
expect(mockSpawn).toHaveBeenCalledWith("agent-browser", expect.any(Array), expect.any(Object));
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|