UNPKG

@truefrontier/puppeteer-laravel-mcp-server

Version:

A Model Context Protocol server for Laravel Herd browser automation using Puppeteer

512 lines 21.5 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { BrowserPool } from '../browser/pool.js'; import { PuppeteerTools } from '../tools/puppeteer-tools.js'; import { LaravelHerdIntegration } from '../laravel/herd-integration.js'; import { serverConfig } from './config.js'; import { readFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8')); export class PuppeteerMCPServer { server; browserPool = null; activeSessions = new Map(); constructor() { this.server = new Server({ name: 'puppeteer-laravel-mcp-server', version: packageJson.version, }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); } async getBrowserPool() { if (!this.browserPool) { this.browserPool = new BrowserPool(); } return this.browserPool; } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Core browser automation tools { name: 'navigate', description: 'Navigate to a URL with Laravel Herd domain resolution', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL to navigate to' }, waitUntil: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'], description: 'When to consider navigation complete' }, timeout: { type: 'number', description: 'Navigation timeout in milliseconds' }, viewport: { type: 'object', properties: { width: { type: 'number' }, height: { type: 'number' } } } }, required: ['url'] } }, { name: 'screenshot', description: 'Capture page screenshot optimized for Claude analysis', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Optional file path' }, type: { type: 'string', enum: ['png', 'jpeg', 'webp'], description: 'Image format' }, quality: { type: 'number', description: 'Image quality (0-100)' }, fullPage: { type: 'boolean', description: 'Capture full page' }, clip: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' }, width: { type: 'number' }, height: { type: 'number' } } } } } }, { name: 'interact', description: 'Perform user interactions (click, type, scroll)', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for element' }, action: { type: 'string', enum: ['click', 'type', 'select', 'scroll'], description: 'Action to perform' }, value: { type: 'string', description: 'Value for type/select actions' }, timeout: { type: 'number', description: 'Timeout in milliseconds' } }, required: ['selector', 'action'] } }, { name: 'execute', description: 'Execute JavaScript in the browser context', inputSchema: { type: 'object', properties: { script: { type: 'string', description: 'JavaScript code to execute' }, args: { type: 'array', description: 'Arguments to pass to script' } }, required: ['script'] } }, { name: 'wait_for', description: 'Wait for elements with intelligent timeout handling', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector to wait for' }, timeout: { type: 'number', description: 'Timeout in milliseconds' }, visible: { type: 'boolean', description: 'Wait for element to be visible' }, hidden: { type: 'boolean', description: 'Wait for element to be hidden' } }, required: ['selector'] } }, // Laravel Herd integration tools { name: 'laravel_auth_login', description: 'Authenticate as a specific user', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'User email to authenticate as' }, guard: { type: 'string', description: 'Laravel guard name' } }, required: ['email'] } }, { name: 'laravel_auth_acting_as', description: 'Act as a specific user by ID', inputSchema: { type: 'object', properties: { userId: { type: 'number', description: 'User ID to act as' }, guard: { type: 'string', description: 'Laravel guard name' } }, required: ['userId'] } }, { name: 'laravel_db_seed', description: 'Run database seeders for test data', inputSchema: { type: 'object', properties: { seeder: { type: 'string', description: 'Seeder class name' }, database: { type: 'string', description: 'Database connection name' } }, required: ['seeder'] } }, { name: 'laravel_route_visit', description: 'Navigate using Laravel route names', inputSchema: { type: 'object', properties: { routeName: { type: 'string', description: 'Laravel route name' }, params: { type: 'object', description: 'Route parameters' } }, required: ['routeName'] } }, { name: 'laravel_artisan', description: 'Execute Laravel Artisan commands', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'Artisan command' }, args: { type: 'array', description: 'Command arguments' } }, required: ['command'] } }, // Session management { name: 'create_session', description: 'Create a new browser session', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Optional session ID' }, siteName: { type: 'string', description: 'Laravel Herd site name' } } } }, { name: 'destroy_session', description: 'Destroy a browser session', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Session ID to destroy' } }, required: ['sessionId'] } }, { name: 'get_page_info', description: 'Get current page information and debug data', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Session ID' } } } }, { name: 'get_pool_stats', description: 'Get browser pool statistics', inputSchema: { type: 'object', properties: {} } } ] }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { // Session management case 'create_session': return await this.createSession(args?.sessionId, args?.siteName); case 'destroy_session': return await this.destroySession(args?.sessionId); case 'get_page_info': return await this.getPageInfo(args?.sessionId); case 'get_pool_stats': return await this.getPoolStats(); // Core browser automation case 'navigate': return await this.navigate(args?.sessionId, args?.url, args); case 'screenshot': return await this.screenshot(args?.sessionId, args); case 'interact': return await this.interact(args?.sessionId, args?.selector, args?.action, args?.value, args); case 'execute': return await this.execute(args?.sessionId, args?.script, args?.args); case 'wait_for': return await this.waitFor(args?.sessionId, args?.selector, args); // Laravel integration case 'laravel_auth_login': return await this.laravelAuthLogin(args?.sessionId, args?.email, args); case 'laravel_auth_acting_as': return await this.laravelAuthActingAs(args?.sessionId, args?.userId, args); case 'laravel_db_seed': return await this.laravelDbSeed(args?.sessionId, args?.seeder, args); case 'laravel_route_visit': return await this.laravelRouteVisit(args?.sessionId, args?.routeName, args?.params); case 'laravel_artisan': return await this.laravelArtisan(args?.sessionId, args?.command, args?.args); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } }); } async getSession(sessionId) { if (!sessionId) { // Create default session if none provided const defaultSessionId = 'default'; if (!this.activeSessions.has(defaultSessionId)) { await this.createSession(defaultSessionId); } sessionId = defaultSessionId; } const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session not found: ${sessionId}`); } return { sessionId, ...session }; } // Session management methods async createSession(sessionId, siteName) { sessionId = sessionId || `session-${Date.now()}`; if (this.activeSessions.has(sessionId)) { throw new Error(`Session already exists: ${sessionId}`); } const browserPool = await this.getBrowserPool(); const instance = await browserPool.acquire(); const page = await browserPool.createPage(instance); const tools = new PuppeteerTools(instance, page); const laravel = new LaravelHerdIntegration(page, siteName); this.activeSessions.set(sessionId, { instance, tools, laravel }); return { content: [ { type: 'text', text: `Session created: ${sessionId}` } ] }; } async destroySession(sessionId) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session not found: ${sessionId}`); } // Close all pages in the browser instance const pages = await session.instance.browser.pages(); await Promise.all(pages.map(page => page.close())); // Release browser instance back to pool const browserPool = await this.getBrowserPool(); await browserPool.release(session.instance); this.activeSessions.delete(sessionId); return { content: [ { type: 'text', text: `Session destroyed: ${sessionId}` } ] }; } async getPageInfo(sessionId) { const { tools } = await this.getSession(sessionId); const info = await tools.getPageInfo(); return { content: [ { type: 'text', text: JSON.stringify(info, null, 2) } ] }; } async getPoolStats() { const browserPool = await this.getBrowserPool(); const stats = browserPool.getStats(); return { content: [ { type: 'text', text: JSON.stringify(stats, null, 2) } ] }; } // Browser automation methods async navigate(sessionId, url, options = {}) { const { tools } = await this.getSession(sessionId); await tools.navigate(url, options); return { content: [ { type: 'text', text: `Navigated to: ${url}` } ] }; } async screenshot(sessionId, options = {}) { const { tools } = await this.getSession(sessionId); const filepath = await tools.screenshot(options); return { content: [ { type: 'text', text: `Screenshot saved: ${filepath}` } ] }; } async interact(sessionId, selector, action, value, options = {}) { const { tools } = await this.getSession(sessionId); await tools.interact(selector, action, value, options); return { content: [ { type: 'text', text: `Interaction completed: ${action} on ${selector}` } ] }; } async execute(sessionId, script, args = []) { const { tools } = await this.getSession(sessionId); const result = await tools.execute(script, ...args); return { content: [ { type: 'text', text: `Script executed. Result: ${JSON.stringify(result)}` } ] }; } async waitFor(sessionId, selector, options = {}) { const { tools } = await this.getSession(sessionId); await tools.waitFor(selector, options); return { content: [ { type: 'text', text: `Element found: ${selector}` } ] }; } // Laravel integration methods async laravelAuthLogin(sessionId, email, options = {}) { const { laravel } = await this.getSession(sessionId); await laravel.loginAs(email, options); return { content: [ { type: 'text', text: `Authenticated as: ${email}` } ] }; } async laravelAuthActingAs(sessionId, userId, options = {}) { const { laravel } = await this.getSession(sessionId); await laravel.actingAs(userId, options); return { content: [ { type: 'text', text: `Acting as user ID: ${userId}` } ] }; } async laravelDbSeed(sessionId, seeder, options = {}) { const { laravel } = await this.getSession(sessionId); await laravel.seedDatabase(seeder, options); return { content: [ { type: 'text', text: `Database seeded with: ${seeder}` } ] }; } async laravelRouteVisit(sessionId, routeName, params = {}) { const { laravel } = await this.getSession(sessionId); await laravel.visitRoute(routeName, params); return { content: [ { type: 'text', text: `Visited route: ${routeName}` } ] }; } async laravelArtisan(sessionId, command, args = []) { const { laravel } = await this.getSession(sessionId); const result = await laravel.runArtisanCommand(command, args); return { content: [ { type: 'text', text: `Artisan command executed: ${command}\n${result}` } ] }; } async start() { if (serverConfig.mcpTransport === 'stdio') { const transport = new StdioServerTransport(); await this.server.connect(transport); } else { throw new Error('HTTP transport not yet implemented'); } } async stop() { // Close all active sessions for (const [sessionId] of this.activeSessions) { await this.destroySession(sessionId); } // Drain browser pool if it was initialized if (this.browserPool) { await this.browserPool.drain(); } // Close server await this.server.close(); } } //# sourceMappingURL=mcp-server.js.map