@webmcp/puppeteer
Version:
Puppeteer integration for webMCP - Headless Chrome automation with AI
324 lines (323 loc) • 12.4 kB
JavaScript
"use strict";
/**
* @webmcp/puppeteer - Puppeteer integration for webMCP
* Headless Chrome automation with AI-driven element detection
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebMCPPuppeteer = void 0;
exports.createWebMCPPuppeteer = createWebMCPPuppeteer;
const puppeteer_1 = __importDefault(require("puppeteer"));
const core_1 = require("@webmcp/core");
const ai_sdk_1 = require("@webmcp/ai-sdk");
class WebMCPPuppeteer {
constructor(config = {}) {
this.config = {
optimizationLevel: 'basic',
screenshotOnError: true,
timeout: 30000,
puppeteerOptions: {
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
},
...config
};
this.processor = new core_1.WebMCPProcessor({
cacheEnabled: true
});
if (config.aiConfig) {
this.aiClient = new ai_sdk_1.WebMCPAIClient({
apiKeys: config.aiConfig.apiKeys,
defaultModel: config.aiConfig.defaultModel || 'gpt-4o',
enableOptimization: true
});
}
}
/**
* Launch browser and create page
*/
async launch() {
this.browser = await puppeteer_1.default.launch(this.config.puppeteerOptions);
this.page = await this.browser.newPage();
// Set viewport and user agent
await this.page.setViewport({ width: 1920, height: 1080 });
await this.page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
return this.page;
}
/**
* Navigate to URL
*/
async goto(url) {
if (!this.page) {
throw new Error('Browser not launched. Call launch() first.');
}
await this.page.goto(url, { waitUntil: 'networkidle0' });
}
/**
* Scan current page and generate webMCP data
*/
async scanPage() {
if (!this.page) {
throw new Error('Browser not launched. Call launch() first.');
}
const html = await this.page.content();
this.webmcpData = this.processor.parseWebMCP(html);
// Add page metadata
const url = this.page.url();
const title = await this.page.title();
if (this.webmcpData.metadata) {
this.webmcpData.metadata.page_url = url;
this.webmcpData.metadata.page_title = title;
}
return this.webmcpData;
}
/**
* Generate AI-driven test actions based on goal
*/
async generateActions(goal) {
if (!this.aiClient) {
throw new Error('AI client not configured. Provide aiConfig to use AI features.');
}
if (!this.webmcpData) {
throw new Error('Page not scanned. Call scanPage() first.');
}
const response = await this.aiClient.processWebMCP(this.webmcpData, goal, {
compressionLevel: this.config.optimizationLevel,
targetModel: this.config.aiConfig?.defaultModel
});
if (response.success && response.response) {
return this.parseAIResponse(response.response);
}
throw new Error('Failed to generate actions: ' + response.error);
}
/**
* Execute webMCP-based actions
*/
async executeActions(actions) {
if (!this.page || !this.webmcpData) {
throw new Error('Page not ready. Call launch() and scanPage() first.');
}
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
console.log(`Executing action ${i + 1}/${actions.length}: ${action}`);
try {
await this.executeAction(action);
await new Promise(resolve => setTimeout(resolve, 500)); // Small delay between actions
}
catch (error) {
if (this.config.screenshotOnError) {
await this.page.screenshot({ path: `puppeteer-error-${i + 1}.png` });
}
throw new Error(`Action ${i + 1} failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
/**
* Smart element interaction using webMCP names
*/
async interactWith(elementName, action, value) {
if (!this.page || !this.webmcpData) {
throw new Error('Page not ready. Call launch() and scanPage() first.');
}
const element = this.webmcpData.elements.find((el) => el.name === elementName || el.name.includes(elementName.toUpperCase()));
if (!element) {
throw new Error(`Element with name '${elementName}' not found in webMCP data`);
}
// Wait for element to be available
await this.page.waitForSelector(element.selector, { timeout: this.config.timeout });
switch (action.toLowerCase()) {
case 'click':
await this.page.click(element.selector);
break;
case 'type':
await this.page.type(element.selector, value || '');
break;
case 'clear':
await this.page.evaluate((selector) => {
const el = document.querySelector(selector);
if (el)
el.value = '';
}, element.selector);
break;
case 'select':
await this.page.select(element.selector, value || '');
break;
case 'hover':
await this.page.hover(element.selector);
break;
case 'focus':
await this.page.focus(element.selector);
break;
case 'screenshot':
const elementHandle = await this.page.$(element.selector);
if (elementHandle) {
await elementHandle.screenshot({ path: `element-${elementName}.png` });
}
break;
default:
throw new Error(`Unknown action: ${action}`);
}
}
/**
* Get element by webMCP name
*/
async getElement(elementName) {
if (!this.page || !this.webmcpData) {
throw new Error('Page not ready. Call launch() and scanPage() first.');
}
const element = this.webmcpData.elements.find((el) => el.name === elementName || el.name.includes(elementName.toUpperCase()));
if (!element) {
throw new Error(`Element with name '${elementName}' not found in webMCP data`);
}
return await this.page.$(element.selector);
}
/**
* Wait for element to appear
*/
async waitForElement(elementName, timeout) {
if (!this.page || !this.webmcpData) {
throw new Error('Page not ready. Call launch() and scanPage() first.');
}
const element = this.webmcpData.elements.find((el) => el.name === elementName || el.name.includes(elementName.toUpperCase()));
if (!element) {
throw new Error(`Element with name '${elementName}' not found in webMCP data`);
}
await this.page.waitForSelector(element.selector, {
timeout: timeout || this.config.timeout
});
}
/**
* Take screenshot of the page
*/
async screenshot(path) {
if (!this.page) {
throw new Error('Browser not launched. Call launch() first.');
}
await this.page.screenshot({
path: path,
fullPage: true
});
}
/**
* Generate PDF of the page
*/
async generatePDF(path) {
if (!this.page) {
throw new Error('Browser not launched. Call launch() first.');
}
await this.page.pdf({
path,
format: 'A4',
printBackground: true
});
}
/**
* Evaluate JavaScript in page context
*/
async evaluate(fn, ...args) {
if (!this.page) {
throw new Error('Browser not launched. Call launch() first.');
}
return await this.page.evaluate(fn, ...args);
}
/**
* Get page performance metrics
*/
async getPerformanceMetrics() {
if (!this.page) {
throw new Error('Browser not launched. Call launch() first.');
}
const metrics = await this.page.metrics();
const performance = await this.page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0];
return {
domContentLoaded: navigation.domContentLoadedEventEnd,
loadComplete: navigation.loadEventEnd,
firstPaint: performance.getEntriesByType('paint').find((p) => p.name === 'first-paint')?.startTime,
firstContentfulPaint: performance.getEntriesByType('paint').find((p) => p.name === 'first-contentful-paint')?.startTime
};
});
return {
puppeteerMetrics: metrics,
performanceTimings: performance,
webmcpMetrics: this.webmcpData ? {
totalElements: this.webmcpData.elements.length,
interactiveElements: this.webmcpData.elements.filter((el) => el.role.includes('input') || el.role.includes('action')).length
} : null
};
}
/**
* Close browser
*/
async close() {
if (this.browser) {
await this.browser.close();
this.browser = undefined;
this.page = undefined;
}
}
parseAIResponse(response) {
const lines = response.split('\\n').filter(line => line.trim());
const actions = [];
lines.forEach(line => {
if (/^\\d+\\./.test(line) || /^(click|type|fill|select|submit|navigate)/i.test(line)) {
actions.push(line.replace(/^\\d+\\.\\s*/, '').trim());
}
});
return actions;
}
async executeAction(action) {
const actionLower = action.toLowerCase();
if (actionLower.includes('click')) {
const elementName = this.extractElementName(action);
await this.interactWith(elementName, 'click');
}
else if (actionLower.includes('type') || actionLower.includes('fill')) {
const elementName = this.extractElementName(action);
const value = this.extractValue(action);
await this.interactWith(elementName, 'type', value);
}
else if (actionLower.includes('select')) {
const elementName = this.extractElementName(action);
const value = this.extractValue(action);
await this.interactWith(elementName, 'select', value);
}
else if (actionLower.includes('navigate')) {
const url = this.extractValue(action);
await this.goto(url);
}
else if (actionLower.includes('wait')) {
const duration = parseInt(this.extractValue(action)) || 1000;
await new Promise(resolve => setTimeout(resolve, duration));
}
else if (actionLower.includes('screenshot')) {
await this.screenshot(`action-screenshot-${Date.now()}.png`);
}
}
extractElementName(action) {
// For type/fill actions, extract element name before "with"
if (action.toLowerCase().includes('type') || action.toLowerCase().includes('fill')) {
const beforeWith = action.split(/\s+with\s+/i)[0];
const matches = beforeWith.match(/\b([A-Z_]+)\b/);
return matches ? matches[1] : '';
}
// For other actions, extract the element name
const matches = action.match(/\b([A-Z_]+)\b/);
return matches ? matches[1] : '';
}
extractValue(action) {
const matches = action.match(/with\s+["']([^"']+)["']/) ||
action.match(/:\s*["']([^"']+)["']/) ||
action.match(/to\s+["']([^"']+)["']/);
return matches ? matches[1] : '';
}
}
exports.WebMCPPuppeteer = WebMCPPuppeteer;
// Export factory function
async function createWebMCPPuppeteer(config) {
const instance = new WebMCPPuppeteer(config);
await instance.launch();
return instance;
}