-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathserver.js
More file actions
335 lines (319 loc) · 15 KB
/
Copy pathserver.js
File metadata and controls
335 lines (319 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
#!/usr/bin/env node
'use strict';
import {FastMCP} from 'fastmcp';
import {z} from 'zod';
import { create_api_headers, poll_task_result, send_session_instructions, create_tool_fn } from './utils.js';
import {createRequire} from 'node:module';
const require = createRequire(import.meta.url);
const package_json = require('./package.json');
const api_token = process.env.API_TOKEN;
const project_name = process.env.PROJECT_NAME;
if (!api_token)
throw new Error('Cannot run MCP server without API_TOKEN env');
if (!project_name)
throw new Error('Cannot run MCP server without PROJECT_NAME env');
let debug_stats = {tool_calls: {}};
const api_headers = create_api_headers(package_json, api_token);
const tool_fn = create_tool_fn(debug_stats);
let server = new FastMCP({
name: 'BrowserAI',
version: package_json.version,
});
function createSessionManager() {
const active_sessions = new Map();
return {
track_session: (id) => {
active_sessions.set(id, {
created: Date.now(),
lastActivity: Date.now()
});
},
update_activity: (id) => {
const session = active_sessions.get(id);
if (session) {
session.lastActivity = Date.now();
}
},
get_sessions: () => Array.from(active_sessions.entries()),
remove_session: (id) => active_sessions.delete(id)
};
}
const sessionManager = createSessionManager();
server.addTool({
name: 'start_new_session',
description: 'Start a new browser session. ' +
'Provide an instruction like "Go to https://example.com" or "Search for products on Amazon". ' +
'Returns executionId for the session and initial page data with interactive elements and HTML markup.',
parameters: z.object({
instruction: z.string(),
geoLocation: z.object({
country: z.string().optional().default('US')
}).optional(),
extractData: z.boolean().optional().default(true)
}),
execute: tool_fn('start_new_session', async ({ instruction, geoLocation, extractData }, { log, reportProgress }) => {
log.info('start_new_session task started', { instruction, geoLocation, extractData });
const url = 'https://browser.ai/api/v1/tasks';
const method = 'POST';
const instructions = [{action: instruction}];
if (extractData)
{
instructions.push({
action: 'Extract all clickable elements, input fields, buttons, and links from the page. ' +
'Also get the complete HTML markup. ' +
'Return the result as a JSON object with this exact format: ' +
'{"interactive_elements": ["element1", "element2", ...], "html_markup": "complete_html_string"}. ' +
'Do not add any extra text or formatting.'
});
}
const body = {
geoLocation: geoLocation || {country: 'US'},
awaitable: true,
instructions,
project: project_name,
type: 'crawler_automation',
};
log.info('Fetching URL', { url, method, instructionsCount: body.instructions.length });
let response = await fetch(url, {
method,
body: JSON.stringify(body),
headers: api_headers(),
});
if (!response.ok)
{
const errorText = await response.text();
log.error('Failed to start new session', { status: response.status, statusText: response.statusText, error: errorText });
throw new Error(`Failed to start new session: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
const task_id = data.executionId;
log.info('Received task ID from API', { task_id, response_data: data });
if (task_id)
{
sessionManager.track_session(task_id);
let result = await poll_task_result(task_id, api_headers, { log, reportProgress, instructions });
return JSON.stringify({executionId: task_id, result});
}
throw new Error('No execution ID received from API');
}),
});
server.addTool({
name: 'interact_and_extract_in_session',
description: 'Interact with elements in an existing browser session. ' +
'Provide an array of instructions like ["Click the login button", "Fill email field with test@example.com", "Scroll down"]. ' +
'Returns updated page data after the interactions.',
parameters: z.object({
instruction: z.string(),
executionId: z.string(),
extractData: z.boolean().optional().default(true),
waitTime: z.number().optional().default(2)
}),
execute: tool_fn('interact_and_extract_in_session', async ({ instruction, executionId, extractData, waitTime }, { log, reportProgress }) => {
log.info('interact_and_extract_in_session task started', { instruction, executionId, extractData, waitTime });
const instructions_payload = [{action: instruction}];
if (waitTime > 0)
instructions_payload.push({action: `Wait ${waitTime} seconds for the page to update after the interaction`});
if (extractData)
{
instructions_payload.push({
action: 'After performing the actions, extract all clickable elements, input fields, buttons, and links from the current page. ' +
'Also get the complete HTML markup. ' +
'Return the result as a JSON object with this exact format: ' +
'{"interactive_elements": ["element1", "element2", ...], "html_markup": "complete_html_string"}. ' +
'Do not add any extra text or formatting.'
});
}
sessionManager.update_activity(executionId);
return await send_session_instructions(
executionId,
instructions_payload,
api_headers,
{ log, reportProgress },
project_name
);
}),
});
server.addTool({
name: 'extract_from_session',
description: 'Extract specific data from the current page in a browser session. ' +
'Provide an array of extraction instructions, e.g., ["Extract all product names and prices as JSON array", "Get the page title and meta description"].',
parameters: z.object({
instruction: z.string(),
executionId: z.string()
}),
execute: tool_fn('extract_from_session', async ({ instruction, executionId }, { log, reportProgress }) => {
log.info('extract_from_session task started', { instruction, executionId });
const instructions_payload = [{action: instruction}];
instructions_payload.push({action: 'Return the extracted data as a clean JSON object. ' +
'No additional text, explanations, or formatting. ' +
'Just the JSON response as specified in the extraction instruction.'});
sessionManager.update_activity(executionId);
return await send_session_instructions(
executionId,
instructions_payload,
api_headers,
{ log, reportProgress },
project_name
);
}),
});
server.addTool({
name: 'get_session_status',
description: 'Check the current status and information of a browser session. ' +
'Useful for debugging or verifying session state.',
parameters: z.object({executionId: z.string()}),
execute: tool_fn('get_session_status', async ({ executionId }, { log, reportProgress }) => {
log.info('get_session_status task started', { executionId });
const url = `https://browser.ai/api/v1/tasks/${executionId}`;
let response = await fetch(url, {
method: 'GET',
headers: api_headers(),
});
if (!response.ok)
{
const errorText = await response.text();
log.error('Failed to get session status', { status: response.status, statusText: response.statusText, error: errorText });
throw new Error(`Failed to get session status: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
return JSON.stringify(data);
}),
});
server.addTool({
name: 'wait_for_element',
description: 'Wait for a specific element to appear on the page before proceeding. ' +
'Useful for dynamic content that loads after page load. ' +
'Provide element selector or description to wait for.',
parameters: z.object({
instruction: z.string(),
executionId: z.string(),
timeout: z.number().optional().default(30)
}),
execute: tool_fn('wait_for_element', async ({ instruction, executionId, timeout }, { log, reportProgress }) => {
log.info('wait_for_element task started', { instruction, executionId, timeout });
const instructionsPayload = [
{action: `Wait up to ${timeout} seconds for this element to appear: ${instruction}`},
{action: 'Once the element is found, extract all clickable elements, input fields, buttons, and links from the current page. ' +
'Also get the complete HTML markup. ' +
'Return the result as a JSON object with this exact format: ' +
'{"interactive_elements": ["element1", "element2", ...], "html_markup": "complete_html_string", "element_found": true}. ' +
'If timeout occurs, return {"element_found": false, "error": "Element not found within timeout"}.'}
];
return await send_session_instructions(
executionId,
instructionsPayload,
api_headers,
{ log, reportProgress },
project_name
);
}),
});
server.addTool({
name: 'navigate_to_url',
description: 'Navigate to a specific URL in an existing browser session. ' +
'Useful for moving between pages while maintaining session state.',
parameters: z.object({
url: z.string(),
executionId: z.string()
}),
execute: tool_fn('navigate_to_url', async ({ url, executionId }, { log, reportProgress }) => {
log.info('navigate_to_url task started', { url, executionId });
const instructionsPayload = [
{action: `Navigate to ${url}`},
{action: 'After navigation completes, extract all clickable elements, input fields, buttons, and links from the page. ' +
'Also get the complete HTML markup and current URL. ' +
'Return the result as a JSON object with this exact format: ' +
'{"interactive_elements": ["element1", "element2", ...], "html_markup": "complete_html_string", "current_url": "actual_url"}. ' +
'Do not add any extra text or formatting.'}
];
return await send_session_instructions(
executionId,
instructionsPayload,
api_headers,
{ log, reportProgress },
project_name
);
}),
});
server.addTool({
name: 'get_page_info',
description: 'Get comprehensive information about the current page including title, URL, meta tags, and page structure. ' +
'Useful for understanding page context before interactions.',
parameters: z.object({executionId: z.string()}),
execute: tool_fn('get_page_info', async ({ executionId }, { log, reportProgress }) => {
log.info('get_page_info task started', { executionId });
const instructionsPayload = [
{action: 'Extract comprehensive page information including title, URL, meta description, meta keywords, and page structure'},
{action: 'Return the page information as a JSON object with this exact format: ' +
'{"title": "page_title", "url": "current_url", "meta_description": "description", "meta_keywords": "keywords", ' +
'"page_structure": {"headings": ["h1", "h2", ...], "forms": ["form1", "form2", ...], "images": ["img1", "img2", ...]}}. ' +
'Do not add any extra text or formatting.'}
];
return await send_session_instructions(
executionId,
instructionsPayload,
api_headers,
{ log, reportProgress },
project_name
);
}),
});
server.addTool({
name: 'batch_actions',
description: 'Execute multiple actions in sequence within a single browser session. ' +
'Provide an array of actions to perform one after another. ' +
'Useful for complex workflows like login -> navigate -> extract data.',
parameters: z.object({
actions: z.array(z.string()),
executionId: z.string(),
stopOnError: z.boolean().optional().default(true),
delayBetweenActions: z.number().optional().default(1)
}),
execute: tool_fn('batch_actions', async ({ actions, executionId, stopOnError, delayBetweenActions }, { log, reportProgress }) => {
log.info('batch_actions task started', { actionsCount: actions.length, executionId, stopOnError, delayBetweenActions });
const instructions_payload = [];
actions.forEach((action, index) => {
instructions_payload.push({action});
if (index < actions.length - 1 && delayBetweenActions > 0) {
instructions_payload.push({action: `Wait ${delayBetweenActions} seconds before next action`});
}
});
instructions_payload.push({
action: 'After completing all actions, extract all clickable elements, input fields, buttons, and links from the final page. ' +
'Also get the complete HTML markup. ' +
'Return the result as a JSON object with this exact format: ' +
'{"interactive_elements": ["element1", "element2", ...], "html_markup": "complete_html_string", "actions_completed": ' + actions.length + '}. ' +
'Do not add any extra text or formatting.'
});
return await send_session_instructions(
executionId,
instructions_payload,
api_headers,
{ log, reportProgress },
project_name
);
}),
});
server.addTool({
name: 'list_active_sessions',
description: 'List all currently active browser sessions with their status and basic information. ' +
'Useful for session management and debugging.',
parameters: z.object({}),
execute: tool_fn('list_active_sessions', async ({}, { log, reportProgress }) => {
log.info('list_active_sessions task started');
const sessions = sessionManager.get_sessions();
const sessionData = sessions.map(([id, data]) => ({
executionId: id,
created: new Date(data.created).toISOString(),
lastActivity: new Date(data.lastActivity).toISOString(),
ageMinutes: Math.round((Date.now() - data.created) / 60000)
}));
return JSON.stringify({
activeSessions: sessionData,
totalSessions: sessionData.length,
timestamp: new Date().toISOString()
});
}),
});
console.error('Starting server...');
server.start({transportType: 'stdio'});