browser-connect-mcp
Version:
MCP server for browser DevTools and backend debugging - analyze console logs, network requests, and backend logs with AI assistance
399 lines • 15.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BackendLogsStreamV2ArgsSchema = void 0;
exports.backendLogsStreamV2 = backendLogsStreamV2;
const zod_1 = require("zod");
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const logger_js_1 = __importDefault(require("../../utils/logger.js"));
const backend_manager_js_1 = require("../../utils/backend-manager.js");
// Enhanced schema with better defaults
exports.BackendLogsStreamV2ArgsSchema = zod_1.z.object({
source: zod_1.z.enum(['auto', 'file', 'process', 'docker', 'start']).default('auto'),
target: zod_1.z.string().optional(), // Optional - we'll auto-detect
command: zod_1.z.string().optional(), // For 'start' source
cwd: zod_1.z.string().optional(),
filters: zod_1.z.object({
level: zod_1.z.array(zod_1.z.enum(['error', 'warn', 'info', 'debug', 'trace'])).optional(),
pattern: zod_1.z.string().optional(),
regex: zod_1.z.boolean().default(false),
since: zod_1.z.string().optional(),
limit: zod_1.z.number().default(1000)
}).optional(),
follow: zod_1.z.boolean().default(true), // Default to following
format: zod_1.z.enum(['json', 'text', 'auto']).default('auto')
});
// Helper to find processes
async function findBackendProcesses() {
return new Promise((resolve) => {
const processes = [];
// Use ps to find Node.js processes
const ps = (0, child_process_1.spawn)('ps', ['aux']);
let output = '';
ps.stdout.on('data', (data) => {
output += data.toString();
});
ps.on('close', () => {
const lines = output.split('\n');
const nodeProcesses = lines.filter(line => line.includes('node') &&
!line.includes('node_modules') &&
!line.includes('browser-connect-mcp'));
for (const line of nodeProcesses) {
const parts = line.split(/\s+/);
if (parts.length > 10) {
processes.push({
pid: parseInt(parts[1]),
name: parts[10],
cmd: parts.slice(10).join(' ')
});
}
}
resolve(processes);
});
});
}
// Helper to find process by port
async function findProcessByPort(port) {
return new Promise((resolve) => {
const lsof = (0, child_process_1.spawn)('lsof', ['-i', `:${port}`, '-n', '-P']);
let output = '';
lsof.stdout.on('data', (data) => {
output += data.toString();
});
lsof.on('close', () => {
const lines = output.split('\n');
for (const line of lines) {
if (line.includes('LISTEN') && line.includes('node')) {
const parts = line.split(/\s+/);
resolve({
pid: parseInt(parts[1]),
name: parts[0],
port: port,
cmd: `node process on port ${port}`
});
return;
}
}
resolve(null);
});
});
}
// Auto-detect log sources
async function detectLogSources(targetHint) {
const sources = [];
const cwd = process.cwd();
// 1. Check for log files
const commonLogPaths = [
'server.log',
'app.log',
'error.log',
'debug.log',
'logs/app.log',
'logs/error.log',
'var/log/app.log',
'.logs/app.log'
];
// If target hint looks like a path, check it first
if (targetHint && (targetHint.includes('/') || targetHint.includes('.log'))) {
if (fs.existsSync(targetHint)) {
sources.push({
type: 'file',
target: targetHint,
description: `Log file: ${targetHint}`
});
}
}
// Check common log locations
for (const logPath of commonLogPaths) {
const fullPath = path.join(cwd, logPath);
if (fs.existsSync(fullPath)) {
sources.push({
type: 'file',
target: fullPath,
description: `Log file: ${logPath}`
});
}
}
// 2. Check for running processes
if (targetHint && /^\d{4,5}$/.test(targetHint)) {
// Port number provided
const process = await findProcessByPort(parseInt(targetHint));
if (process) {
sources.push({
type: 'process',
target: process.pid.toString(),
description: `Process on port ${targetHint} (PID: ${process.pid})`
});
}
}
// Check for Node.js processes
const processes = await findBackendProcesses();
for (const proc of processes) {
sources.push({
type: 'process',
target: proc.pid.toString(),
description: `${proc.name} (PID: ${proc.pid})`
});
}
// 3. Check for Docker containers
try {
const docker = (0, child_process_1.spawn)('docker', ['ps', '--format', '{{.Names}}']);
let containers = '';
docker.stdout.on('data', (data) => {
containers += data.toString();
});
await new Promise(resolve => docker.on('close', resolve));
if (containers) {
const containerList = containers.trim().split('\n');
for (const container of containerList) {
if (container) {
sources.push({
type: 'docker',
target: container,
description: `Docker container: ${container}`
});
}
}
}
}
catch (error) {
// Docker not available
}
return sources;
}
// Start a new process with debugging
async function startProcessWithDebugging(command, cwd) {
const logs = [];
// Parse command to add debugging flags
let [cmd, ...args] = command.split(' ');
// Add debugging flag for Node.js
if (cmd === 'node' && !args.includes('--inspect')) {
args.unshift('--inspect');
}
// Handle npm/yarn commands
if (cmd === 'npm' && args[0] === 'start') {
// Try to get the actual command from package.json
try {
const packageJson = JSON.parse(fs.readFileSync(path.join(cwd || process.cwd(), 'package.json'), 'utf-8'));
if (packageJson.scripts?.start) {
// Recursively handle the start script
return startProcessWithDebugging(packageJson.scripts.start, cwd);
}
}
catch (error) {
// Continue with npm start
}
}
const proc = (0, child_process_1.spawn)(cmd, args, {
cwd: cwd || process.cwd(),
env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'development' }
});
// Capture stdout
proc.stdout.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
for (const line of lines) {
logs.push({
timestamp: new Date().toISOString(),
level: 'info',
message: line,
source: 'stdout',
raw: line
});
}
});
// Capture stderr
proc.stderr.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
for (const line of lines) {
// Check if it's actually an error or just debug output
const isError = line.toLowerCase().includes('error') ||
line.toLowerCase().includes('exception');
logs.push({
timestamp: new Date().toISOString(),
level: isError ? 'error' : 'debug',
message: line,
source: 'stderr',
raw: line
});
}
});
return { process: proc, logs };
}
// Main function with enhanced capabilities
async function backendLogsStreamV2(args) {
const backendManager = backend_manager_js_1.BackendManager.getInstance();
try {
// Auto-detect source if needed
if (args.source === 'auto') {
const sources = await detectLogSources(args.target);
if (sources.length === 0) {
// No sources found, provide guidance
return {
success: false,
error: 'No log sources found',
suggestions: [
'Start your backend with: node --inspect server.js',
'Or ensure your app writes logs to a file',
'Or run your app in a Docker container'
],
help: 'Would you like me to start your backend with debugging enabled?'
};
}
// Use the first source (or best match based on target hint)
const source = sources[0];
args.source = source.type;
args.target = source.target;
logger_js_1.default.info('Auto-detected log source', {
type: source.type,
target: source.target,
description: source.description
});
}
// Handle the start source
if (args.source === 'start') {
if (!args.command) {
// Try to detect start command
const packageJsonPath = path.join(process.cwd(), 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
if (packageJson.scripts?.start) {
args.command = 'npm start';
}
else if (packageJson.main) {
args.command = `node ${packageJson.main}`;
}
}
if (!args.command) {
// Look for common entry points
const entryPoints = ['server.js', 'app.js', 'index.js', 'src/index.js'];
for (const entry of entryPoints) {
if (fs.existsSync(entry)) {
args.command = `node ${entry}`;
break;
}
}
}
if (!args.command) {
return {
success: false,
error: 'Could not determine start command',
suggestions: [
'Specify the command: { "command": "node server.js" }',
'Or add a start script to package.json'
]
};
}
}
// Start the process
const { process, logs } = await startProcessWithDebugging(args.command, args.cwd);
return {
success: true,
source: 'start',
command: args.command,
pid: process.pid,
message: `Started process with PID ${process.pid}`,
logs: logs.slice(0, args.filters?.limit || 1000),
streaming: args.follow,
tip: 'Process is running with debugging enabled. Logs will be captured in real-time.'
};
}
// Handle other sources (file, process, docker)
let logs = [];
switch (args.source) {
case 'file':
if (!args.target) {
return {
success: false,
error: 'File path required for file source'
};
}
logs = await streamFileLog(args.target, args.filters, args.format, args.follow);
break;
case 'process':
if (!args.target) {
return {
success: false,
error: 'Process ID or port required for process source'
};
}
logs = await streamProcessLog(args.target, args.filters, args.follow);
break;
case 'docker':
if (!args.target) {
return {
success: false,
error: 'Container name required for docker source'
};
}
logs = await streamDockerLog(args.target, args.filters, args.follow);
break;
}
// Store and filter logs
if (args.target) {
backendManager.addLogs(args.target, logs);
}
if (args.filters) {
logs = filterLogs(logs, args.filters);
}
return {
success: true,
source: args.source,
target: args.target,
logCount: logs.length,
logs: logs.slice(0, args.filters?.limit || 1000),
streaming: args.follow
};
}
catch (error) {
logger_js_1.default.error('Failed to stream logs', { error });
return {
isError: true,
error: error instanceof Error ? error.message : 'Failed to stream logs',
suggestions: [
'Check if the target file/process exists',
'Ensure you have necessary permissions',
'Try using the "start" source to launch with debugging'
]
};
}
}
// ... (Include the existing helper functions from the original file)
// streamFileLog, streamProcessLog, streamDockerLog, parseLogLine, filterLogs, etc.
//# sourceMappingURL=logs-stream-v2.js.map