@truefrontier/puppeteer-laravel-mcp-server
Version:
A Model Context Protocol server for Laravel Herd browser automation using Puppeteer
512 lines • 21.5 kB
JavaScript
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