UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

516 lines (515 loc) 20.9 kB
#!/usr/bin/env node /** * Enhanced Playwright MCP Server with Authentication * Includes token-based authentication and authorization */ const { chromium } = require('playwright'); const readline = require('readline'); const MCPAuthMiddleware = require('./auth-middleware.js'); let AuthenticatedPlaywrightMCPServer = class AuthenticatedPlaywrightMCPServer { constructor(options = {}){ this.browser = null; this.page = null; this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); // Initialize authentication middleware this.auth = new MCPAuthMiddleware({ // FIX: Default to 'localhost' for host execution, Docker deployments should set CFN_REDIS_HOST explicitly redisUrl: options.redisUrl || process.env.CFN_REDIS_URL || process.env.MCP_REDIS_URL || `redis://${process.env.CFN_REDIS_HOST || 'localhost'}:${process.env.CFN_REDIS_PORT || 6379}`, authRequired: options.authRequired !== false && process.env.MCP_AUTH_REQUIRED !== 'false', agentConfigPath: options.agentConfigPath || process.env.MCP_AGENT_CONFIG || './config/agent-whitelist.json', skillConfigPath: options.skillConfigPath || process.env.MCP_SKILL_CONFIG || './config/skill-requirements.json', rateLimitMax: options.rateLimitMax || parseInt(process.env.MCP_RATE_LIMIT_MAX) || 60, rateLimitWindow: options.rateLimitWindow || parseInt(process.env.MCP_RATE_LIMIT_WINDOW) || 60 }); this.tools = { take_screenshot: { name: 'take_screenshot', description: 'Take a screenshot of a webpage (requires browser-automation, screenshot-capture skills)', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL to capture' }, filename: { type: 'string', description: 'Screenshot filename (optional)' }, fullPage: { type: 'boolean', default: false, description: 'Capture full page' }, waitTime: { type: 'number', default: 3000, description: 'Wait time before screenshot (ms)' } }, required: [ 'url' ] }, skillRequirements: [ 'browser-automation', 'screenshot-capture' ] }, search_google: { name: 'search_google', description: 'Search Google and return results (requires browser-automation, web-search skills)', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, screenshot: { type: 'boolean', default: true, description: 'Take screenshot of results' }, resultCount: { type: 'number', default: 5, description: 'Number of results to return' } }, required: [ 'query' ] }, skillRequirements: [ 'browser-automation', 'web-search' ] }, navigate_and_interact: { name: 'navigate_and_interact', description: 'Navigate to a webpage and interact with elements (requires browser-automation, web-interaction skills)', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL to navigate to' }, actions: { type: 'array', description: 'Array of actions to perform', items: { type: 'object', properties: { type: { type: 'string', enum: [ 'click', 'fill', 'press', 'wait' ] }, selector: { type: 'string', description: 'CSS selector' }, value: { type: 'string', description: 'Value for fill actions' }, key: { type: 'string', description: 'Key for press actions' } } } }, screenshot: { type: 'boolean', default: false } }, required: [ 'url' ] }, skillRequirements: [ 'browser-automation', 'web-interaction' ] } }; this.stats = { totalRequests: 0, authorizedRequests: 0, rejectedRequests: 0, toolUsage: {}, lastActivity: null }; } async initialize() { try { // Initialize authentication await this.auth.initialize(); console.error('[MCP-Server] Authentication middleware initialized'); // Initialize Playwright browser await this.initBrowser(); console.error('[MCP-Server] Playwright browser initialized'); console.error('[MCP-Server] Authenticated Playwright MCP Server ready'); } catch (error) { console.error('[MCP-Server] Failed to initialize:', error); process.exit(1); } } async initBrowser() { if (!this.browser) { this.browser = await chromium.launch({ headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu' ] }); this.page = await this.browser.newPage(); } } async takeScreenshot(args, agentContext) { await this.initBrowser(); console.error(`[MCP-Server] Taking screenshot for ${agentContext.agentType}`); await this.page.goto(args.url, { waitUntil: 'networkidle', timeout: 15000 }); if (args.waitTime > 0) { await this.page.waitForTimeout(args.waitTime); } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = args.filename || `screenshot-${agentContext.agentType}-${timestamp}.png`; const filepath = `/app/screenshots/${filename}`; await this.page.screenshot({ path: filepath, fullPage: args.fullPage || false }); return { success: true, filename: filename, filepath: filepath, url: args.url, title: await this.page.title(), size: `${this.page.viewportSize()?.width || 1280}x${this.page.viewportSize()?.height || 720}`, capturedBy: agentContext.agentType, timestamp: new Date().toISOString() }; } async searchGoogle(args, agentContext) { await this.initBrowser(); console.error(`[MCP-Server] Google search for ${agentContext.agentType}: "${args.query}"`); await this.page.goto('https://www.google.com', { waitUntil: 'networkidle', timeout: 15000 }); // Handle cookies popup if present try { await this.page.waitForSelector('button[aria-label*="Accept"], button[aria-label*="agree"]', { timeout: 3000 }); await this.page.click('button[aria-label*="Accept"], button[aria-label*="agree"]'); await this.page.waitForTimeout(1000); } catch (e) { // No cookie popup, continue } // Perform search const searchBox = await this.page.waitForSelector('textarea[name="q"], input[name="q"]', { timeout: 10000 }); await searchBox.fill(args.query); await searchBox.press('Enter'); // Wait for results await this.page.waitForSelector('[role="main"], #search', { timeout: 15000 }); // Extract results const results = await this.page.$$eval('div[data-hveid] h3', (elements)=>elements.slice(0, args.resultCount || 5).map((el)=>el.textContent.trim())); let screenshotInfo = null; if (args.screenshot) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `google-search-${args.query.toLowerCase().replace(/\s+/g, '-')}-${timestamp}.png`; await this.page.screenshot({ path: `/app/screenshots/${filename}` }); screenshotInfo = { filename, path: `/app/screenshots/${filename}`, url: this.page.url() }; } return { success: true, query: args.query, results: results, resultCount: results.length, screenshot: screenshotInfo, url: this.page.url(), title: await this.page.title(), searchedBy: agentContext.agentType, timestamp: new Date().toISOString() }; } async navigateAndInteract(args, agentContext) { await this.initBrowser(); console.error(`[MCP-Server] Page interaction for ${agentContext.agentType}: ${args.url}`); await this.page.goto(args.url, { waitUntil: 'networkidle', timeout: 15000 }); const results = []; for (const action of args.actions || []){ try { switch(action.type){ case 'click': await this.page.waitForSelector(action.selector, { timeout: 5000 }); await this.page.click(action.selector); results.push({ action: 'click', selector: action.selector, success: true }); break; case 'fill': await this.page.waitForSelector(action.selector, { timeout: 5000 }); await this.page.fill(action.selector, action.value); results.push({ action: 'fill', selector: action.selector, value: action.value, success: true }); break; case 'press': await this.page.press(action.selector || 'body', action.key); results.push({ action: 'press', key: action.key, success: true }); break; case 'wait': await this.page.waitForTimeout(parseInt(action.value) || 1000); results.push({ action: 'wait', duration: action.value, success: true }); break; } } catch (error) { results.push({ action: action.type, selector: action.selector, success: false, error: error.message }); } } let screenshotInfo = null; if (args.screenshot) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `interaction-${agentContext.agentType}-${timestamp}.png`; await this.page.screenshot({ path: `/app/screenshots/${filename}` }); screenshotInfo = { filename, path: `/app/screenshots/${filename}` }; } return { success: true, url: args.url, actions: results, screenshot: screenshotInfo, title: await this.page.title(), interactedBy: agentContext.agentType, timestamp: new Date().toISOString() }; } async handleMessage(message) { try { this.stats.totalRequests++; this.stats.lastActivity = Date.now(); // Initialize request context const request = { headers: { 'x-agent-token': message.params?.context?.agentToken, 'x-agent-type': message.params?.context?.agentType }, body: message }; const response = { writeHead: ()=>{}, end: (data)=>{ // For JSON-RPC, we'll just return the data directly this.responseData = JSON.parse(data); } }; // Authenticate request let authenticationPassed = false; await this.auth.authenticateRequest(request, response, ()=>{ authenticationPassed = true; }); if (!authenticationPassed) { this.stats.rejectedRequests++; return this.responseData || { jsonrpc: '2.0', id: message.id, error: { code: -32001, message: 'Authentication failed' } }; } this.stats.authorizedRequests++; // Authentication passed, process request if (message.method === 'initialize') { return { jsonrpc: '2.0', id: message.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'playwright-mcp-server-authenticated', version: '1.0.0', authentication: 'enabled' } } }; } else if (message.method === 'tools/list') { return { jsonrpc: '2.0', id: message.id, result: { tools: Object.values(this.tools) } }; } else if (message.method === 'tools/call') { const toolName = message.params.name; const args = message.params.arguments || {}; const agentContext = request.agentContext || { agentType: 'unknown', authenticated: false }; // Update tool usage stats this.stats.toolUsage[toolName] = (this.stats.toolUsage[toolName] || 0) + 1; let result; if (toolName === 'take_screenshot') { result = await this.takeScreenshot(args, agentContext); } else if (toolName === 'search_google') { result = await this.searchGoogle(args, agentContext); } else if (toolName === 'navigate_and_interact') { result = await this.navigateAndInteract(args, agentContext); } else { throw new Error(`Unknown tool: ${toolName}`); } console.error(`[MCP-Server] Tool ${toolName} executed by ${agentContext.agentType}`); return { jsonrpc: '2.0', id: message.id, result: { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] } }; } } catch (error) { console.error('[MCP-Server] Error handling message:', error); return { jsonrpc: '2.0', id: message.id || null, error: { code: -32000, message: error.message } }; } } async start() { await this.initialize(); this.rl.on('line', async (line)=>{ try { const message = JSON.parse(line); const response = await this.handleMessage(message); console.log(JSON.stringify(response)); } catch (error) { console.log(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: `Parse error: ${error.message}` } })); } }); // Cleanup on exit process.on('SIGINT', async ()=>{ console.error('[MCP-Server] Shutting down authenticated Playwright MCP Server...'); await this.shutdown(); process.exit(0); }); // Log statistics periodically setInterval(()=>{ console.error(`[MCP-Server] Stats: ${this.stats.totalRequests} total, ${this.stats.authorizedRequests} authorized, ${this.stats.rejectedRequests} rejected`); console.error(`[MCP-Server] Tool usage:`, JSON.stringify(this.stats.toolUsage)); }, 60000); // Every minute } async getStats() { const authStats = await this.auth.getStats(); return { server: { name: 'playwright-mcp-server-authenticated', uptime: process.uptime(), ...this.stats }, authentication: authStats }; } async shutdown() { try { if (this.browser) await this.browser.close(); if (this.rl) this.rl.close(); if (this.auth) await this.auth.shutdown(); console.error('[MCP-Server] Shutdown complete'); } catch (error) { console.error('[MCP-Server] Error during shutdown:', error); } } }; // Start the server const server = new AuthenticatedPlaywrightMCPServer({ redisUrl: process.env.MCP_REDIS_URL, authRequired: process.env.MCP_AUTH_REQUIRED !== 'false', rateLimitMax: parseInt(process.env.MCP_RATE_LIMIT_MAX) || 60 }); server.start().catch((error)=>{ console.error('[MCP-Server] Failed to start authenticated Playwright MCP Server:', error); process.exit(1); }); //# sourceMappingURL=playwright-mcp-server-auth.js.map