@darbotlabs/darbot-browser-mcp
Version:
🤖 Your Autonomous Browser Companion - 29 AI-driven browser tools with work profile support and VS Code GitHub Copilot agent mode integration
272 lines (271 loc) • 10.9 kB
JavaScript
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import path from 'path';
import os from 'os';
import { z } from 'zod';
import { defineTool } from './tool.js';
import { sanitizeForFilePath } from './utils.js';
const saveProfileSchema = z.object({
name: z.string().describe('Name for the work profile'),
description: z.string().optional().describe('Optional description for the work profile'),
});
const switchProfileSchema = z.object({
name: z.string().describe('Name of the work profile to switch to'),
});
const listProfilesSchema = z.object({});
const deleteProfileSchema = z.object({
name: z.string().describe('Name of the work profile to delete'),
});
async function getProfilesDir() {
let profilesDir;
if (process.platform === 'linux')
profilesDir = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
else if (process.platform === 'darwin')
profilesDir = path.join(os.homedir(), 'Library', 'Application Support');
else if (process.platform === 'win32')
profilesDir = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
else
throw new Error('Unsupported platform: ' + process.platform);
const result = path.join(profilesDir, 'darbot-browser-mcp', 'work-profiles');
await fs.promises.mkdir(result, { recursive: true });
return result;
}
async function saveCurrentProfile(context, profileName, description) {
const profilesDir = await getProfilesDir();
const sanitizedName = sanitizeForFilePath(profileName);
const profileDir = path.join(profilesDir, sanitizedName);
await fs.promises.mkdir(profileDir, { recursive: true });
// Get current browser state
const tab = context.currentTabOrDie();
const url = tab.page.url();
const title = await tab.title();
// Save profile metadata
const profileData = {
name: profileName,
description: description || '',
created: new Date().toISOString(),
url,
title,
};
await fs.promises.writeFile(path.join(profileDir, 'profile.json'), JSON.stringify(profileData, null, 2));
// Save storage state (cookies, localStorage, etc.)
try {
const storageState = await tab.page.context().storageState();
await fs.promises.writeFile(path.join(profileDir, 'storage-state.json'), JSON.stringify(storageState, null, 2));
}
catch (error) {
// Storage state save failed, but we can still save the profile
// eslint-disable-next-line no-console
console.warn('Failed to save storage state:', error);
}
return profileData;
}
async function loadProfile(context, profileName) {
const profilesDir = await getProfilesDir();
const sanitizedName = sanitizeForFilePath(profileName);
const profileDir = path.join(profilesDir, sanitizedName);
try {
await fs.promises.access(profileDir);
}
catch {
throw new Error(`Work profile "${profileName}" not found`);
}
// Load profile metadata
const profileDataPath = path.join(profileDir, 'profile.json');
const profileData = JSON.parse(await fs.promises.readFile(profileDataPath, 'utf8'));
// Load storage state if available
const storageStatePath = path.join(profileDir, 'storage-state.json');
try {
await fs.promises.access(storageStatePath);
const storageState = JSON.parse(await fs.promises.readFile(storageStatePath, 'utf8'));
// Create new context with the stored state
const tab = await context.ensureTab();
const currentContext = tab.page.context();
if (currentContext) {
await currentContext.close();
}
const newContext = await tab.page.context().browser()?.newContext({
storageState,
viewport: null,
});
if (newContext) {
const newPage = await newContext.newPage();
await newPage.goto(profileData.url);
return { profileData, restored: true };
}
}
catch {
// Storage state not available or failed to load, fall through to fallback
}
// Fallback: just navigate to the URL
const tab = await context.ensureTab();
await tab.page.goto(profileData.url);
return { profileData, restored: false };
}
async function listProfiles() {
const profilesDir = await getProfilesDir();
const profiles = [];
try {
const entries = await fs.promises.readdir(profilesDir);
for (const entry of entries) {
const profileDir = path.join(profilesDir, entry);
const stat = await fs.promises.stat(profileDir);
if (stat.isDirectory()) {
const profileDataPath = path.join(profileDir, 'profile.json');
try {
await fs.promises.access(profileDataPath);
const profileData = JSON.parse(await fs.promises.readFile(profileDataPath, 'utf8'));
profiles.push(profileData);
}
catch {
// File does not exist, skip this entry
}
}
}
}
catch (error) {
// Profiles directory doesn't exist yet
return [];
}
return profiles;
}
async function deleteProfile(profileName) {
const profilesDir = await getProfilesDir();
const sanitizedName = sanitizeForFilePath(profileName);
const profileDir = path.join(profilesDir, sanitizedName);
try {
await fs.promises.access(profileDir);
}
catch {
throw new Error(`Work profile "${profileName}" not found`);
}
await fs.promises.rm(profileDir, { recursive: true, force: true });
}
export const browserSaveProfile = defineTool({
capability: 'core',
schema: {
name: 'browser_save_profile',
title: 'Autonomous profile saving',
description: 'Autonomously save the current browser state as a reusable work profile for later restoration',
inputSchema: saveProfileSchema,
type: 'destructive',
},
handle: async (context, { name, description }) => {
const profileData = await saveCurrentProfile(context, name, description);
return {
code: [`await browser_save_profile({ name: '${name}', description: '${description || ''}' })`],
action: async () => ({ content: [] }),
captureSnapshot: false,
waitForNetwork: false,
resultOverride: {
content: [{
type: 'text',
text: `Work profile "${name}" saved successfully.\n\nProfile details:\n- Name: ${profileData.name}\n- Description: ${profileData.description}\n- URL: ${profileData.url}\n- Title: ${profileData.title}\n- Created: ${profileData.created}`,
}],
},
};
},
});
export const browserSwitchProfile = defineTool({
capability: 'core',
schema: {
name: 'browser_switch_profile',
title: 'Autonomous profile switching',
description: 'Autonomously switch to a previously saved work profile, restoring browser state and session',
inputSchema: switchProfileSchema,
type: 'destructive',
},
handle: async (context, { name }) => {
const result = await loadProfile(context, name);
return {
code: [`await browser_switch_profile({ name: '${name}' })`],
action: async () => ({ content: [] }),
captureSnapshot: true,
waitForNetwork: false,
resultOverride: {
content: [{
type: 'text',
text: `Switched to work profile "${name}".\n\nProfile details:\n- Name: ${result.profileData.name}\n- Description: ${result.profileData.description}\n- URL: ${result.profileData.url}\n- Title: ${result.profileData.title}\n- Storage state ${result.restored ? 'restored' : 'not available'}`,
}],
},
};
},
});
export const browserListProfiles = defineTool({
capability: 'core',
schema: {
name: 'browser_list_profiles',
title: 'Autonomous profile listing',
description: 'Autonomously list all saved work profiles with their details and creation information',
inputSchema: listProfilesSchema,
type: 'readOnly',
},
handle: async (context, {}) => {
const profiles = await listProfiles();
let text = '### Saved Work Profiles\n\n';
if (profiles.length === 0) {
text += 'No work profiles saved yet. Use the "browser_save_profile" tool to save your current browser state as a work profile.';
}
else {
for (const profile of profiles) {
text += `**${profile.name}**\n`;
if (profile.description)
text += `- Description: ${profile.description}\n`;
text += `- URL: ${profile.url}\n`;
text += `- Title: ${profile.title}\n`;
text += `- Created: ${new Date(profile.created).toLocaleString()}\n\n`;
}
}
return {
code: ['await browser_list_profiles()'],
action: async () => ({ content: [] }),
captureSnapshot: false,
waitForNetwork: false,
resultOverride: {
content: [{
type: 'text',
text,
}],
},
};
},
});
export const browserDeleteProfile = defineTool({
capability: 'core',
schema: {
name: 'browser_delete_profile',
title: 'Autonomous profile deletion',
description: 'Autonomously delete a saved work profile permanently from storage',
inputSchema: deleteProfileSchema,
type: 'destructive',
},
handle: async (context, { name }) => {
await deleteProfile(name);
return {
code: [`await browser_delete_profile({ name: '${name}' })`],
action: async () => ({ content: [] }),
captureSnapshot: false,
waitForNetwork: false,
resultOverride: {
content: [{
type: 'text',
text: `Work profile "${name}" deleted successfully.`,
}],
},
};
},
});