@ricardodeazambuja/browser-mcp-server 1.0.3 → 1.4.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 (43) hide show
  1. package/CHANGELOG-v1.3.0.md +42 -0
  2. package/CHANGELOG-v1.4.0.md +8 -0
  3. package/README.md +271 -45
  4. package/package.json +11 -10
  5. package/plugins/.gitkeep +0 -0
  6. package/src/.gitkeep +0 -0
  7. package/src/browser.js +152 -0
  8. package/src/cdp.js +58 -0
  9. package/src/index.js +126 -0
  10. package/src/tools/.gitkeep +0 -0
  11. package/src/tools/console.js +139 -0
  12. package/src/tools/docs.js +1611 -0
  13. package/src/tools/index.js +60 -0
  14. package/src/tools/info.js +139 -0
  15. package/src/tools/interaction.js +126 -0
  16. package/src/tools/keyboard.js +27 -0
  17. package/src/tools/media.js +264 -0
  18. package/src/tools/mouse.js +104 -0
  19. package/src/tools/navigation.js +72 -0
  20. package/src/tools/network.js +552 -0
  21. package/src/tools/pages.js +149 -0
  22. package/src/tools/performance.js +517 -0
  23. package/src/tools/security.js +470 -0
  24. package/src/tools/storage.js +467 -0
  25. package/src/tools/system.js +196 -0
  26. package/src/utils.js +131 -0
  27. package/tests/.gitkeep +0 -0
  28. package/tests/fixtures/.gitkeep +0 -0
  29. package/tests/fixtures/test-media.html +35 -0
  30. package/tests/fixtures/test-network.html +48 -0
  31. package/tests/fixtures/test-performance.html +61 -0
  32. package/tests/fixtures/test-security.html +33 -0
  33. package/tests/fixtures/test-storage.html +76 -0
  34. package/tests/run-all.js +50 -0
  35. package/{test-browser-automation.js → tests/test-browser-automation.js} +44 -5
  36. package/{test-mcp.js → tests/test-mcp.js} +9 -4
  37. package/tests/test-media-tools.js +168 -0
  38. package/tests/test-network.js +212 -0
  39. package/tests/test-performance.js +254 -0
  40. package/tests/test-security.js +203 -0
  41. package/tests/test-storage.js +192 -0
  42. package/CHANGELOG-v1.0.2.md +0 -126
  43. package/browser-mcp-server-playwright.js +0 -792
@@ -0,0 +1,60 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const tools = [];
5
+ const handlers = {};
6
+
7
+ // Default tool modules to load
8
+ const coreModules = [
9
+ 'navigation',
10
+ 'interaction',
11
+ 'mouse',
12
+ 'keyboard',
13
+ 'pages',
14
+ 'media',
15
+ 'console',
16
+ 'info',
17
+ 'system',
18
+ 'docs',
19
+ 'performance',
20
+ 'network',
21
+ 'security',
22
+ 'storage'
23
+ ];
24
+
25
+ // Load core tools
26
+ coreModules.forEach(name => {
27
+ try {
28
+ const mod = require(`./${name}.js`);
29
+ if (mod.definitions) {
30
+ tools.push(...mod.definitions);
31
+ }
32
+ if (mod.handlers) {
33
+ Object.assign(handlers, mod.handlers);
34
+ }
35
+ } catch (error) {
36
+ console.error(`Failed to load core tool module: ${name}`, error);
37
+ }
38
+ });
39
+
40
+ // Auto-load plugins from ../../plugins/ if they exist
41
+ const pluginsDir = path.join(__dirname, '..', '..', 'plugins');
42
+ if (fs.existsSync(pluginsDir)) {
43
+ fs.readdirSync(pluginsDir)
44
+ .filter(f => f.endsWith('.js'))
45
+ .forEach(f => {
46
+ try {
47
+ const mod = require(path.join(pluginsDir, f));
48
+ if (mod.definitions) {
49
+ tools.push(...mod.definitions);
50
+ }
51
+ if (mod.handlers) {
52
+ Object.assign(handlers, mod.handlers);
53
+ }
54
+ } catch (error) {
55
+ console.error(`Failed to load plugin: ${f}`, error);
56
+ }
57
+ });
58
+ }
59
+
60
+ module.exports = { tools, handlers };
@@ -0,0 +1,139 @@
1
+ const { connectToBrowser } = require('../browser');
2
+
3
+ const definitions = [
4
+ {
5
+ name: 'browser_screenshot',
6
+ description: 'Take a screenshot of the current page (see browser_docs)',
7
+ inputSchema: {
8
+ type: 'object',
9
+ properties: {
10
+ fullPage: { type: 'boolean', description: 'Capture full page', default: false }
11
+ },
12
+ additionalProperties: false,
13
+ $schema: 'http://json-schema.org/draft-07/schema#'
14
+ }
15
+ },
16
+ {
17
+ name: 'browser_get_text',
18
+ description: 'Get text content from an element (see browser_docs)',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ selector: { type: 'string', description: 'Playwright selector for the element' }
23
+ },
24
+ required: ['selector'],
25
+ additionalProperties: false,
26
+ $schema: 'http://json-schema.org/draft-07/schema#'
27
+ }
28
+ },
29
+ {
30
+ name: 'browser_evaluate',
31
+ description: 'Execute JavaScript in the browser context (see browser_docs)',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ code: { type: 'string', description: 'JavaScript code to execute' }
36
+ },
37
+ required: ['code'],
38
+ additionalProperties: false,
39
+ $schema: 'http://json-schema.org/draft-07/schema#'
40
+ }
41
+ },
42
+ {
43
+ name: 'browser_get_dom',
44
+ description: 'Get the full DOM structure or specific element data (see browser_docs)',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ selector: { type: 'string', description: 'Optional selector to get DOM of specific element' }
49
+ },
50
+ additionalProperties: false,
51
+ $schema: 'http://json-schema.org/draft-07/schema#'
52
+ }
53
+ },
54
+ {
55
+ name: 'browser_read_page',
56
+ description: 'Read the content and metadata of the current page (see browser_docs)',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {},
60
+ additionalProperties: false,
61
+ $schema: 'http://json-schema.org/draft-07/schema#'
62
+ }
63
+ }
64
+ ];
65
+
66
+ const handlers = {
67
+ browser_screenshot: async (args) => {
68
+ const { page } = await connectToBrowser();
69
+ const screenshot = await page.screenshot({
70
+ fullPage: args.fullPage || false,
71
+ type: 'png'
72
+ });
73
+ return {
74
+ content: [{
75
+ type: 'image',
76
+ data: screenshot.toString('base64'),
77
+ mimeType: 'image/png'
78
+ }]
79
+ };
80
+ },
81
+
82
+ browser_get_text: async (args) => {
83
+ const { page } = await connectToBrowser();
84
+ const text = await page.textContent(args.selector);
85
+ return { content: [{ type: 'text', text }] };
86
+ },
87
+
88
+ browser_evaluate: async (args) => {
89
+ const { page } = await connectToBrowser();
90
+ const result = await page.evaluate(args.code);
91
+ return {
92
+ content: [{
93
+ type: 'text',
94
+ text: JSON.stringify(result, null, 2)
95
+ }]
96
+ };
97
+ },
98
+
99
+ browser_get_dom: async (args) => {
100
+ const { page } = await connectToBrowser();
101
+ const domContent = await page.evaluate((sel) => {
102
+ const element = sel ? document.querySelector(sel) : document.documentElement;
103
+ if (!element) return null;
104
+ return {
105
+ outerHTML: element.outerHTML,
106
+ textContent: element.textContent,
107
+ attributes: Array.from(element.attributes || []).map(attr => ({
108
+ name: attr.name,
109
+ value: attr.value
110
+ })),
111
+ children: element.children.length
112
+ };
113
+ }, args.selector);
114
+ return {
115
+ content: [{
116
+ type: 'text',
117
+ text: JSON.stringify(domContent, null, 2)
118
+ }]
119
+ };
120
+ },
121
+
122
+ browser_read_page: async (args) => {
123
+ const { page } = await connectToBrowser();
124
+ const metadata = {
125
+ title: await page.title(),
126
+ url: page.url(),
127
+ viewport: page.viewportSize(),
128
+ contentLength: (await page.content()).length
129
+ };
130
+ return {
131
+ content: [{
132
+ type: 'text',
133
+ text: JSON.stringify(metadata, null, 2)
134
+ }]
135
+ };
136
+ }
137
+ };
138
+
139
+ module.exports = { definitions, handlers };
@@ -0,0 +1,126 @@
1
+ const { connectToBrowser } = require('../browser');
2
+
3
+ const definitions = [
4
+ {
5
+ name: 'browser_click',
6
+ description: 'Click an element on the page using Playwright selector (see browser_docs)',
7
+ inputSchema: {
8
+ type: 'object',
9
+ properties: {
10
+ selector: { type: 'string', description: 'Playwright selector for the element' }
11
+ },
12
+ required: ['selector'],
13
+ additionalProperties: false,
14
+ $schema: 'http://json-schema.org/draft-07/schema#'
15
+ }
16
+ },
17
+ {
18
+ name: 'browser_type',
19
+ description: 'Type text into an input field (see browser_docs)',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ selector: { type: 'string', description: 'Playwright selector for the input' },
24
+ text: { type: 'string', description: 'Text to type' }
25
+ },
26
+ required: ['selector', 'text'],
27
+ additionalProperties: false,
28
+ $schema: 'http://json-schema.org/draft-07/schema#'
29
+ }
30
+ },
31
+ {
32
+ name: 'browser_hover',
33
+ description: 'Hover over an element (see browser_docs)',
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {
37
+ selector: { type: 'string', description: 'Playwright selector for the element' }
38
+ },
39
+ required: ['selector'],
40
+ additionalProperties: false,
41
+ $schema: 'http://json-schema.org/draft-07/schema#'
42
+ }
43
+ },
44
+ {
45
+ name: 'browser_focus',
46
+ description: 'Focus an element (see browser_docs)',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ selector: { type: 'string', description: 'Playwright selector for the element' }
51
+ },
52
+ required: ['selector'],
53
+ additionalProperties: false,
54
+ $schema: 'http://json-schema.org/draft-07/schema#'
55
+ }
56
+ },
57
+ {
58
+ name: 'browser_select',
59
+ description: 'Select options in a dropdown (see browser_docs)',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ selector: { type: 'string', description: 'Playwright selector for the select element' },
64
+ values: { type: 'array', items: { type: 'string' }, description: 'Values to select' }
65
+ },
66
+ required: ['selector', 'values'],
67
+ additionalProperties: false,
68
+ $schema: 'http://json-schema.org/draft-07/schema#'
69
+ }
70
+ },
71
+ {
72
+ name: 'browser_scroll',
73
+ description: 'Scroll the page (see browser_docs)',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ x: { type: 'number', description: 'Horizontal scroll position' },
78
+ y: { type: 'number', description: 'Vertical scroll position' }
79
+ },
80
+ additionalProperties: false,
81
+ $schema: 'http://json-schema.org/draft-07/schema#'
82
+ }
83
+ }
84
+ ];
85
+
86
+ const handlers = {
87
+ browser_click: async (args) => {
88
+ const { page } = await connectToBrowser();
89
+ await page.click(args.selector);
90
+ return { content: [{ type: 'text', text: `Clicked ${args.selector}` }] };
91
+ },
92
+ browser_type: async (args) => {
93
+ const { page } = await connectToBrowser();
94
+ await page.fill(args.selector, args.text);
95
+ return { content: [{ type: 'text', text: `Typed into ${args.selector}` }] };
96
+ },
97
+ browser_hover: async (args) => {
98
+ const { page } = await connectToBrowser();
99
+ await page.hover(args.selector);
100
+ return { content: [{ type: 'text', text: `Hovered over ${args.selector}` }] };
101
+ },
102
+ browser_focus: async (args) => {
103
+ const { page } = await connectToBrowser();
104
+ await page.focus(args.selector);
105
+ return { content: [{ type: 'text', text: `Focused ${args.selector}` }] };
106
+ },
107
+ browser_select: async (args) => {
108
+ const { page } = await connectToBrowser();
109
+ await page.selectOption(args.selector, args.values);
110
+ return { content: [{ type: 'text', text: `Selected values in ${args.selector}` }] };
111
+ },
112
+ browser_scroll: async (args) => {
113
+ const { page } = await connectToBrowser();
114
+ await page.evaluate(({ x, y }) => {
115
+ window.scrollTo(x || 0, y || 0);
116
+ }, args);
117
+ return {
118
+ content: [{
119
+ type: 'text',
120
+ text: `Scrolled to (${args.x || 0}, ${args.y || 0})`
121
+ }]
122
+ };
123
+ }
124
+ };
125
+
126
+ module.exports = { definitions, handlers };
@@ -0,0 +1,27 @@
1
+ const { connectToBrowser } = require('../browser');
2
+
3
+ const definitions = [
4
+ {
5
+ name: 'browser_press_key',
6
+ description: 'Send a keyboard event (press a key) (see browser_docs)',
7
+ inputSchema: {
8
+ type: 'object',
9
+ properties: {
10
+ key: { type: 'string', description: 'The key to press (e.g., "Enter", "Escape", "Control+A")' }
11
+ },
12
+ required: ['key'],
13
+ additionalProperties: false,
14
+ $schema: 'http://json-schema.org/draft-07/schema#'
15
+ }
16
+ }
17
+ ];
18
+
19
+ const handlers = {
20
+ browser_press_key: async (args) => {
21
+ const { page } = await connectToBrowser();
22
+ await page.keyboard.press(args.key);
23
+ return { content: [{ type: 'text', text: `Pressed key: ${args.key}` }] };
24
+ }
25
+ };
26
+
27
+ module.exports = { definitions, handlers };
@@ -0,0 +1,264 @@
1
+ const { connectToBrowser } = require('../browser');
2
+
3
+ const definitions = [
4
+ {
5
+ name: 'browser_get_media_summary',
6
+ description: 'Get a summary of all audio and video elements on the page (see browser_docs)',
7
+ inputSchema: {
8
+ type: 'object',
9
+ properties: {},
10
+ additionalProperties: false,
11
+ $schema: 'http://json-schema.org/draft-07/schema#'
12
+ }
13
+ },
14
+ {
15
+ name: 'browser_get_audio_analysis',
16
+ description: 'Analyze audio output for a duration to detect sound vs silence and frequencies (see browser_docs)',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ durationMs: { type: 'number', description: 'Duration to analyze in ms', default: 2000 },
21
+ selector: { type: 'string', description: 'Optional selector to specific media element' }
22
+ },
23
+ additionalProperties: false,
24
+ $schema: 'http://json-schema.org/draft-07/schema#'
25
+ }
26
+ },
27
+ {
28
+ name: 'browser_control_media',
29
+ description: 'Control a media element (play, pause, seek, mute) (see browser_docs)',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ selector: { type: 'string', description: 'Selector for the audio/video element' },
34
+ action: { type: 'string', enum: ['play', 'pause', 'mute', 'unmute', 'seek'] },
35
+ value: { type: 'number', description: 'Value for seek action (time in seconds)' }
36
+ },
37
+ required: ['selector', 'action'],
38
+ additionalProperties: false,
39
+ $schema: 'http://json-schema.org/draft-07/schema#'
40
+ }
41
+ }
42
+ ];
43
+
44
+ const handlers = {
45
+ browser_get_media_summary: async (args) => {
46
+ const { page } = await connectToBrowser();
47
+ const mediaState = await page.evaluate(() => {
48
+ const elements = Array.from(document.querySelectorAll('audio, video'));
49
+ return elements.map((el, index) => {
50
+ // Calculate buffered ranges
51
+ const buffered = [];
52
+ for (let i = 0; i < el.buffered.length; i++) {
53
+ buffered.push([el.buffered.start(i), el.buffered.end(i)]);
54
+ }
55
+
56
+ return {
57
+ index,
58
+ tagName: el.tagName.toLowerCase(),
59
+ id: el.id || null,
60
+ src: el.currentSrc || el.src,
61
+ state: {
62
+ paused: el.paused,
63
+ muted: el.muted,
64
+ ended: el.ended,
65
+ loop: el.loop,
66
+ playbackRate: el.playbackRate,
67
+ volume: el.volume
68
+ },
69
+ timing: {
70
+ currentTime: el.currentTime,
71
+ duration: el.duration
72
+ },
73
+ buffer: {
74
+ readyState: el.readyState,
75
+ buffered
76
+ },
77
+ videoSpecs: el.tagName === 'VIDEO' ? {
78
+ videoWidth: el.videoWidth,
79
+ videoHeight: el.videoHeight
80
+ } : undefined
81
+ };
82
+ });
83
+ });
84
+ return {
85
+ content: [{
86
+ type: 'text',
87
+ text: JSON.stringify(mediaState, null, 2)
88
+ }]
89
+ };
90
+ },
91
+
92
+ browser_get_audio_analysis: async (args) => {
93
+ const { page } = await connectToBrowser();
94
+ const duration = args.durationMs || 2000;
95
+ const selector = args.selector;
96
+
97
+ const analysis = await page.evaluate(async ({ duration, selector }) => {
98
+ return new Promise(async (resolve) => {
99
+ try {
100
+ // 1. Find element
101
+ let element;
102
+ if (selector) {
103
+ element = document.querySelector(selector);
104
+ } else {
105
+ // Pick the first one playing or just the first one
106
+ const all = Array.from(document.querySelectorAll('audio, video'));
107
+ element = all.find(e => !e.paused) || all[0];
108
+ }
109
+
110
+ if (!element) return resolve({ error: 'No media element found' });
111
+
112
+ // 2. Setup AudioContext (handle browser policies)
113
+ const CtxClass = window.AudioContext || window.webkitAudioContext;
114
+ if (!CtxClass) return resolve({ error: 'Web Audio API not supported' });
115
+
116
+ const ctx = new CtxClass();
117
+
118
+ // Resume context if suspended (common in browsers)
119
+ if (ctx.state === 'suspended') await ctx.resume();
120
+
121
+ // 3. Create Source & Analyzer
122
+ // Note: MediaElementSource requires the element to allow CORS if cross-origin
123
+ let source;
124
+ try {
125
+ source = ctx.createMediaElementSource(element);
126
+ } catch (e) {
127
+ // If already connected or tainted, this might fail.
128
+ // We can try to reconnect or just capture current data if available.
129
+ return resolve({ error: `Cannot connect to media source: ${e.message}. (Check CORS headers)` });
130
+ }
131
+
132
+ const analyzer = ctx.createAnalyser();
133
+ analyzer.fftSize = 256;
134
+ const bufferLength = analyzer.frequencyBinCount;
135
+ const dataArray = new Uint8Array(bufferLength);
136
+
137
+ source.connect(analyzer);
138
+ analyzer.connect(ctx.destination);
139
+
140
+ // 4. Collect samples over duration
141
+ const samples = [];
142
+ const startTime = Date.now();
143
+ const interval = setInterval(() => {
144
+ analyzer.getByteFrequencyData(dataArray);
145
+
146
+ // Calculate instant stats
147
+ let sum = 0;
148
+ let max = 0;
149
+ for (let i = 0; i < bufferLength; i++) {
150
+ const val = dataArray[i];
151
+ sum += val;
152
+ if (val > max) max = val;
153
+ }
154
+ const avg = sum / bufferLength;
155
+
156
+ samples.push({ avg, max, data: Array.from(dataArray) });
157
+
158
+ if (Date.now() - startTime >= duration) {
159
+ clearInterval(interval);
160
+ finalize();
161
+ }
162
+ }, 100); // 10 samples per second
163
+
164
+ function finalize() {
165
+ // Clean up
166
+ try {
167
+ source.disconnect();
168
+ analyzer.disconnect();
169
+ ctx.close();
170
+ } catch (e) { }
171
+
172
+ // Aggregate
173
+ if (samples.length === 0) return resolve({ status: 'No samples' });
174
+
175
+ const totalAvg = samples.reduce((a, b) => a + b.avg, 0) / samples.length;
176
+ const grandMax = Math.max(...samples.map(s => s.max));
177
+ const isSilent = grandMax < 5; // Low threshold
178
+
179
+ // Bucket frequencies (Simple approximation)
180
+ // 128 bins over Nyquist (e.g. 24kHz).
181
+ // Bass (0-4), Mid (5-40), Treble (41-127) roughly
182
+ const bassSum = samples.reduce((acc, s) => {
183
+ return acc + s.data.slice(0, 5).reduce((a, b) => a + b, 0) / 5;
184
+ }, 0) / samples.length;
185
+
186
+ const midSum = samples.reduce((acc, s) => {
187
+ return acc + s.data.slice(5, 40).reduce((a, b) => a + b, 0) / 35;
188
+ }, 0) / samples.length;
189
+
190
+ const trebleSum = samples.reduce((acc, s) => {
191
+ return acc + s.data.slice(40).reduce((a, b) => a + b, 0) / 88;
192
+ }, 0) / samples.length;
193
+
194
+ const activeFrequencies = [];
195
+ if (bassSum > 20) activeFrequencies.push('bass');
196
+ if (midSum > 20) activeFrequencies.push('mid');
197
+ if (trebleSum > 20) activeFrequencies.push('treble');
198
+
199
+ resolve({
200
+ element: { tagName: element.tagName, id: element.id, src: element.currentSrc },
201
+ isSilent,
202
+ averageVolume: Math.round(totalAvg),
203
+ peakVolume: grandMax,
204
+ activeFrequencies
205
+ });
206
+ }
207
+
208
+ } catch (e) {
209
+ resolve({ error: e.message });
210
+ }
211
+ });
212
+ }, { duration, selector });
213
+
214
+ return {
215
+ content: [{
216
+ type: 'text',
217
+ text: JSON.stringify(analysis, null, 2)
218
+ }]
219
+ };
220
+ },
221
+
222
+ browser_control_media: async (args) => {
223
+ const { page } = await connectToBrowser();
224
+ const controlResult = await page.evaluate(async ({ selector, action, value }) => {
225
+ const el = document.querySelector(selector);
226
+ if (!el) return { error: `Element not found: ${selector}` };
227
+ if (!(el instanceof HTMLMediaElement)) return { error: 'Element is not audio/video' };
228
+
229
+ try {
230
+ switch (action) {
231
+ case 'play':
232
+ await el.play();
233
+ return { status: 'playing' };
234
+ case 'pause':
235
+ el.pause();
236
+ return { status: 'paused' };
237
+ case 'mute':
238
+ el.muted = true;
239
+ return { status: 'muted' };
240
+ case 'unmute':
241
+ el.muted = false;
242
+ return { status: 'unmuted' };
243
+ case 'seek':
244
+ if (typeof value !== 'number') return { error: 'Seek value required' };
245
+ el.currentTime = value;
246
+ return { status: 'seeked', newTime: el.currentTime };
247
+ default:
248
+ return { error: `Unknown media action: ${action}` };
249
+ }
250
+ } catch (e) {
251
+ return { error: e.message };
252
+ }
253
+ }, args);
254
+
255
+ return {
256
+ content: [{
257
+ type: 'text',
258
+ text: JSON.stringify(controlResult, null, 2)
259
+ }]
260
+ };
261
+ }
262
+ };
263
+
264
+ module.exports = { definitions, handlers };