@chinchillaenterprises/mcp-dev-logger 2.3.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,2131 +4,316 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { z } from "zod";
6
6
  import { spawn, exec } from "child_process";
7
- import { writeFileSync, readFileSync, existsSync, appendFileSync, copyFileSync, readdirSync, statSync, mkdirSync, rmSync } from "fs";
8
- import { join, resolve } from "path";
9
- import { tmpdir } from "os";
7
+ import { writeFileSync, existsSync, appendFileSync, mkdirSync, readFileSync } from "fs";
8
+ import { join } from "path";
10
9
  import { promisify } from "util";
11
- import { chromium } from "playwright";
12
10
  const execAsync = promisify(exec);
13
- // Schema definitions
14
- const StartLogStreamingArgsSchema = z.object({
15
- command: z.string().optional().describe("Dev command to run (default: npm run dev)"),
16
- outputFile: z.string().optional().describe("File to write logs to (DEPRECATED: use structuredLogging instead)"),
17
- cwd: z.string().optional().describe("Working directory"),
18
- env: z.record(z.string()).optional().describe("Environment variables"),
19
- processId: z.string().optional().describe("Custom process ID (auto-generated if not provided)"),
20
- structuredLogging: z.boolean().optional().describe("Use organized logging with date-based sessions (default: true)"),
21
- sessionDate: z.string().optional().describe("Session date for organized logging (YYYY-MM-DD, defaults to today)"),
22
- logType: z.enum(["frontend", "backend", "amplify", "custom"]).optional().describe("Process type for standardized naming (auto-detected if not provided)")
11
+ // Simple schema - no parameters needed
12
+ const StartSandboxArgsSchema = z.object({});
13
+ const StopSandboxArgsSchema = z.object({});
14
+ const TailSandboxLogsArgsSchema = z.object({
15
+ lines: z.number().optional().describe("Number of lines to return (default: 50)")
23
16
  });
24
- const StopLogStreamingArgsSchema = z.object({
25
- processId: z.string().optional().describe("Process ID to stop (if not provided, stops all processes)")
26
- });
27
- const RestartLogStreamingArgsSchema = z.object({
28
- processId: z.string().describe("Process ID to restart"),
29
- clearLogs: z.boolean().optional().describe("Clear logs on restart (default: false)")
30
- });
31
- const TailLogsArgsSchema = z.object({
32
- processId: z.string().optional().describe("Process ID to tail logs from (if not provided, tries to find single process)"),
33
- lines: z.number().optional().describe("Number of lines to return (default: 50)"),
34
- filter: z.string().optional().describe("Grep pattern to filter logs")
35
- });
36
- const ClearLogsArgsSchema = z.object({
37
- processId: z.string().optional().describe("Process ID to clear logs for (if not provided, tries to find single process)"),
38
- backup: z.boolean().optional().describe("Backup logs before clearing (default: false)")
39
- });
40
- const GetProcessesArgsSchema = z.object({
41
- processId: z.string().optional().describe("Process ID to get info for (if not provided, shows all processes)"),
42
- format: z.enum(["raw", "formatted"]).optional().describe("Output format: raw (milliseconds) or formatted (hours/minutes/seconds) - default: formatted")
43
- });
44
- const CheckRunningProcessesArgsSchema = z.object({
45
- processType: z.enum(["frontend", "backend", "amplify", "custom"]).optional().describe("Type of process to check for conflicts"),
46
- port: z.number().optional().describe("Specific port to check")
47
- });
48
- const DiscoverLogsArgsSchema = z.object({
49
- sessionDate: z.string().optional().describe("Specific session date (YYYY-MM-DD) to discover logs for (defaults to most recent)")
50
- });
51
- const StartFrontendWithBrowserArgsSchema = z.object({
52
- command: z.string().optional().describe("Dev command to run (default: npm run dev)"),
53
- port: z.number().optional().describe("Port to wait for (default: auto-detect from output)"),
54
- waitTimeout: z.number().optional().describe("Max time to wait for server in ms (default: 30000)"),
55
- browserDelay: z.number().optional().describe("Delay after server ready in ms (default: 1000)"),
56
- teachingMode: z.boolean().optional().describe("Enable teaching features (default: true)"),
57
- processId: z.string().optional().describe("Custom process ID (default: frontend-with-browser)"),
58
- env: z.record(z.string()).optional().describe("Environment variables"),
59
- cwd: z.string().optional().describe("Working directory")
60
- });
61
- class DevLoggerServer {
62
- server;
63
- activeServers;
64
- pidFile;
65
- constructor() {
66
- this.server = new Server({
67
- name: "mcp-dev-logger",
68
- version: "1.0.0",
69
- }, {
70
- capabilities: {
71
- tools: {},
72
- },
73
- });
74
- this.activeServers = new Map();
75
- this.pidFile = join(tmpdir(), "mcp-dev-logger.json");
76
- // Load any saved state
77
- this.loadState();
78
- this.setupHandlers();
17
+ const HARDCODED_COMMAND = "npx ampx sandbox --stream-function-logs";
18
+ let sandboxProcess = null;
19
+ // Get log file path - always in resources/sandbox/sandbox.log relative to cwd
20
+ function getLogFilePath() {
21
+ const cwd = process.cwd();
22
+ const sandboxDir = join(cwd, "resources", "sandbox");
23
+ // Create resources/sandbox directory if it doesn't exist
24
+ if (!existsSync(sandboxDir)) {
25
+ mkdirSync(sandboxDir, { recursive: true });
79
26
  }
80
- loadState() {
81
- try {
82
- if (existsSync(this.pidFile)) {
83
- const data = JSON.parse(readFileSync(this.pidFile, 'utf8'));
84
- // Note: We can't restore the actual process objects, but we can track that they existed
85
- console.error("Previous dev server sessions detected. Use dev_get_status to check.");
27
+ return join(sandboxDir, "sandbox.log");
28
+ }
29
+ // Find existing sandbox process for current directory
30
+ async function findExistingSandbox() {
31
+ const currentDir = process.cwd();
32
+ try {
33
+ // Get all ampx sandbox processes
34
+ const { stdout } = await execAsync("ps aux | grep 'ampx sandbox' | grep -v grep");
35
+ if (!stdout.trim()) {
36
+ return null; // No sandboxes running
37
+ }
38
+ // Extract PIDs from ps output
39
+ const lines = stdout.trim().split('\n');
40
+ const pids = [];
41
+ for (const line of lines) {
42
+ const match = line.match(/^\S+\s+(\d+)/);
43
+ if (match) {
44
+ pids.push(parseInt(match[1]));
86
45
  }
87
46
  }
88
- catch (error) {
89
- // Ignore errors loading state
90
- }
91
- }
92
- saveState() {
93
- try {
94
- const state = {};
95
- for (const [id, info] of this.activeServers) {
96
- state[id] = {
97
- command: info.command,
98
- cwd: info.cwd,
99
- outputFile: info.outputFile,
100
- startTime: info.startTime,
101
- pid: info.pid,
102
- processId: info.processId
103
- };
47
+ // Check each PID's working directory
48
+ for (const pid of pids) {
49
+ try {
50
+ const { stdout: lsofOutput } = await execAsync(`lsof -a -d cwd -p ${pid} 2>/dev/null | tail -1`);
51
+ // Extract path from lsof output (fields 9 onwards)
52
+ const fields = lsofOutput.trim().split(/\s+/);
53
+ const pathFields = fields.slice(8); // Starting from index 8 (field 9 in 1-indexed)
54
+ const workingDir = pathFields.join(' ').trim();
55
+ if (workingDir === currentDir) {
56
+ return pid; // Found our sandbox!
57
+ }
58
+ }
59
+ catch (err) {
60
+ // Process might have exited, continue checking others
61
+ continue;
104
62
  }
105
- writeFileSync(this.pidFile, JSON.stringify(state, null, 2));
106
- }
107
- catch (error) {
108
- // Ignore errors saving state
109
63
  }
64
+ return null; // No sandbox found for this directory
110
65
  }
111
- generateProcessId(command, outputFile) {
112
- // Create a readable process ID based on command and output file
113
- const commandPart = command.split(' ')[0].replace(/[^a-zA-Z0-9]/g, '');
114
- const filePart = outputFile.split('/').pop()?.replace(/[^a-zA-Z0-9]/g, '').replace(/\.(txt|log)$/, '') || 'default';
115
- return `${commandPart}-${filePart}`;
116
- }
117
- findProcessOrDefault(processId) {
118
- if (processId) {
119
- return this.activeServers.has(processId) ? processId : null;
120
- }
121
- // If no processId provided and only one process running, use that
122
- const activeIds = Array.from(this.activeServers.keys());
123
- if (activeIds.length === 1) {
124
- return activeIds[0];
125
- }
66
+ catch (err) {
67
+ // No processes found or error occurred
126
68
  return null;
127
69
  }
128
- async checkPortInUse(port) {
70
+ }
71
+ // Start Amplify sandbox with streaming logs
72
+ async function startSandbox() {
73
+ // Check for existing sandbox process in this directory
74
+ const existingPid = await findExistingSandbox();
75
+ const logFile = getLogFilePath();
76
+ let killedPid;
77
+ if (existingPid) {
78
+ // Found existing sandbox, kill it first
129
79
  try {
130
- // Use lsof to check what's using the port
131
- const { stdout } = await execAsync(`lsof -i :${port} -P -t`);
132
- const pid = parseInt(stdout.trim());
133
- if (pid) {
134
- // Get command for this PID
135
- try {
136
- const { stdout: cmdStdout } = await execAsync(`ps -p ${pid} -o command=`);
137
- return {
138
- inUse: true,
139
- pid: pid,
140
- command: cmdStdout.trim()
141
- };
142
- }
143
- catch {
144
- return { inUse: true, pid: pid };
145
- }
146
- }
80
+ process.kill(existingPid, 'SIGTERM');
81
+ killedPid = existingPid;
82
+ const timestamp = new Date().toISOString();
83
+ appendFileSync(logFile, `[${timestamp}] Killed existing sandbox (PID: ${existingPid}), starting fresh...\n`, "utf-8");
84
+ // Wait a bit for cleanup
85
+ await new Promise(resolve => setTimeout(resolve, 2000));
147
86
  }
148
- catch (error) {
149
- // Port is not in use or lsof failed
87
+ catch (err) {
88
+ // Process might already be dead, continue anyway
150
89
  }
151
- return { inUse: false };
152
- }
153
- detectProcessType(command) {
154
- const cmd = command.toLowerCase();
155
- if (cmd.includes('next dev') || cmd.includes('npm run dev') || cmd.includes('yarn dev') || cmd.includes('pnpm dev') || cmd.includes('vite')) {
156
- return 'frontend';
157
- }
158
- if (cmd.includes('ampx sandbox') || cmd.includes('amplify sandbox')) {
159
- return 'amplify';
160
- }
161
- if (cmd.includes('node server') || cmd.includes('express') || cmd.includes('fastify') || cmd.includes('api')) {
162
- return 'backend';
163
- }
164
- return 'custom';
165
- }
166
- getCommonPorts(processType) {
167
- switch (processType) {
168
- case 'frontend':
169
- return [3000, 3001, 5173, 5174, 8080, 8081];
170
- case 'backend':
171
- return [3001, 8000, 8080, 4000, 5000];
172
- case 'amplify':
173
- return []; // Amplify doesn't typically use fixed ports
174
- default:
175
- return [3000, 3001, 5173, 5174, 8000, 8080, 8081, 4000, 5000];
90
+ // Clear our tracked process if it was the one we killed
91
+ if (sandboxProcess?.pid === existingPid) {
92
+ sandboxProcess = null;
176
93
  }
177
94
  }
178
- async checkRunningProcesses(processType, specificPort) {
179
- const conflicts = [];
180
- const recommendations = [];
181
- // Ports to check
182
- const portsToCheck = specificPort ? [specificPort] : this.getCommonPorts(processType);
183
- // Check each port
184
- for (const port of portsToCheck) {
185
- const portCheck = await this.checkPortInUse(port);
186
- if (portCheck.inUse) {
187
- const detectedType = portCheck.command ? this.detectProcessType(portCheck.command) : 'unknown';
188
- conflicts.push({
189
- port: port,
190
- pid: portCheck.pid || 0,
191
- command: portCheck.command || 'Unknown process',
192
- processType: detectedType
193
- });
194
- recommendations.push(`Port ${port} is busy with ${detectedType} server (PID ${portCheck.pid})`);
95
+ // Start new sandbox
96
+ // Clear/overwrite the log file
97
+ writeFileSync(logFile, "", "utf-8");
98
+ // Write initial log entry
99
+ const timestamp = new Date().toISOString();
100
+ appendFileSync(logFile, `[${timestamp}] Starting Amplify sandbox: ${HARDCODED_COMMAND}\n`, "utf-8");
101
+ // Start the process
102
+ const cwd = process.cwd();
103
+ const proc = spawn("npx", ["ampx", "sandbox", "--stream-function-logs"], {
104
+ cwd,
105
+ stdio: ["ignore", "pipe", "pipe"],
106
+ detached: false,
107
+ shell: false
108
+ });
109
+ sandboxProcess = {
110
+ process: proc,
111
+ pid: proc.pid,
112
+ startTime: new Date()
113
+ };
114
+ // Stream stdout to log file
115
+ proc.stdout?.on("data", (data) => {
116
+ const timestamp = new Date().toISOString();
117
+ const lines = data.toString().split("\n");
118
+ lines.forEach(line => {
119
+ if (line.trim()) {
120
+ appendFileSync(logFile, `[${timestamp}] ${line}\n`, "utf-8");
195
121
  }
196
- }
197
- // Find available ports
198
- const allCommonPorts = [3000, 3001, 3002, 5173, 5174, 8000, 8080, 8081, 4000, 5000];
199
- const busyPorts = conflicts.map(c => c.port);
200
- const availablePorts = allCommonPorts.filter(port => !busyPorts.includes(port));
201
- // Add recommendations
202
- if (conflicts.length > 0 && availablePorts.length > 0) {
203
- recommendations.push(`Consider using port ${availablePorts[0]} instead`);
204
- recommendations.push(`Or stop the existing process first`);
205
- }
206
- if (conflicts.length === 0) {
207
- recommendations.push('No conflicts detected - safe to start new processes');
208
- }
122
+ });
123
+ });
124
+ // Stream stderr to log file
125
+ proc.stderr?.on("data", (data) => {
126
+ const timestamp = new Date().toISOString();
127
+ const lines = data.toString().split("\n");
128
+ lines.forEach(line => {
129
+ if (line.trim()) {
130
+ appendFileSync(logFile, `[${timestamp}] [ERROR] ${line}\n`, "utf-8");
131
+ }
132
+ });
133
+ });
134
+ // Handle process exit
135
+ proc.on("exit", (code, signal) => {
136
+ const timestamp = new Date().toISOString();
137
+ appendFileSync(logFile, `[${timestamp}] Amplify sandbox exited with code ${code} and signal ${signal}\n`, "utf-8");
138
+ sandboxProcess = null;
139
+ });
140
+ proc.on("error", (err) => {
141
+ const timestamp = new Date().toISOString();
142
+ appendFileSync(logFile, `[${timestamp}] [ERROR] Failed to start Amplify sandbox: ${err.message}\n`, "utf-8");
143
+ sandboxProcess = null;
144
+ });
145
+ const message = killedPid
146
+ ? `Killed existing sandbox (PID: ${killedPid}) and started fresh. Logs are being written to ${logFile}`
147
+ : `Amplify sandbox started. Logs are being written to ${logFile}`;
148
+ return {
149
+ success: true,
150
+ message,
151
+ logFile,
152
+ killedPid
153
+ };
154
+ }
155
+ // Stop Amplify sandbox
156
+ function stopSandbox() {
157
+ if (!sandboxProcess?.process) {
209
158
  return {
210
- status: 'success',
211
- conflicts,
212
- availablePorts: availablePorts.slice(0, 5), // Limit to first 5 available
213
- recommendations
159
+ success: false,
160
+ message: "No Amplify sandbox process is running."
214
161
  };
215
162
  }
216
- detectLogProcessType(fileName) {
217
- const name = fileName.toLowerCase();
218
- if (name.includes('frontend') || name.includes('next') || name.includes('react') || name.includes('vite')) {
219
- return 'frontend';
220
- }
221
- if (name.includes('backend') || name.includes('api') || name.includes('server')) {
222
- return 'backend';
223
- }
224
- if (name.includes('amplify') || name.includes('ampx')) {
225
- return 'amplify';
226
- }
227
- return 'custom';
228
- }
229
- getLogFilesFromDirectory(dirPath) {
230
- if (!existsSync(dirPath)) {
231
- return [];
232
- }
233
- const logFiles = [];
234
- try {
235
- const files = readdirSync(dirPath);
236
- for (const file of files) {
237
- // Skip non-log files and hidden files
238
- if (!file.endsWith('.log') && !file.endsWith('.txt') || file.startsWith('.')) {
239
- continue;
240
- }
241
- const filePath = join(dirPath, file);
242
- const stats = statSync(filePath);
243
- // Get line count for log files
244
- let lineCount;
245
- try {
246
- const content = readFileSync(filePath, 'utf8');
247
- lineCount = content.split('\n').filter(line => line.trim()).length;
248
- }
249
- catch {
250
- // Ignore errors reading file content
251
- }
252
- logFiles.push({
253
- fileName: file,
254
- filePath: filePath,
255
- processType: this.detectLogProcessType(file),
256
- size: stats.size,
257
- lastModified: stats.mtime,
258
- lineCount
259
- });
260
- }
261
- }
262
- catch (error) {
263
- // Directory read error - return empty array
264
- }
265
- return logFiles.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
163
+ const logFile = getLogFilePath();
164
+ const timestamp = new Date().toISOString();
165
+ appendFileSync(logFile, `[${timestamp}] Stopping Amplify sandbox...\n`, "utf-8");
166
+ try {
167
+ sandboxProcess.process.kill("SIGTERM");
168
+ sandboxProcess = null;
169
+ return {
170
+ success: true,
171
+ message: "Amplify sandbox stopped successfully."
172
+ };
266
173
  }
267
- async discoverLogs(sessionDate) {
268
- const cwd = process.cwd();
269
- const logsDir = join(cwd, 'logs');
270
- // If logs directory doesn't exist, check current directory for log files
271
- if (!existsSync(logsDir)) {
272
- const currentDirLogs = this.getLogFilesFromDirectory(cwd);
273
- if (currentDirLogs.length === 0) {
274
- return {
275
- status: 'no_logs_found',
276
- availableSessions: [],
277
- recommendedLogs: ['No log files found. Start a dev server to create logs.'],
278
- totalSessions: 0
279
- };
280
- }
281
- // Return current directory logs as a session
282
- return {
283
- status: 'success',
284
- mostRecentSession: {
285
- sessionDate: new Date().toISOString().split('T')[0],
286
- logFiles: currentDirLogs,
287
- totalFiles: currentDirLogs.length,
288
- sessionPath: cwd
289
- },
290
- availableSessions: ['current'],
291
- recommendedLogs: currentDirLogs.map(f => `${f.processType}: ${f.fileName} (${f.lineCount || 0} lines)`),
292
- totalSessions: 1
293
- };
294
- }
295
- // Discover date-based sessions in logs directory
296
- const sessions = [];
297
- let mostRecentSession;
298
- try {
299
- const entries = readdirSync(logsDir);
300
- for (const entry of entries) {
301
- const entryPath = join(logsDir, entry);
302
- const stats = statSync(entryPath);
303
- if (stats.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry)) {
304
- sessions.push(entry);
305
- }
306
- }
307
- // Sort sessions by date (most recent first)
308
- sessions.sort((a, b) => b.localeCompare(a));
309
- // Get the target session (specified or most recent)
310
- const targetSession = sessionDate || sessions[0];
311
- if (targetSession && sessions.includes(targetSession)) {
312
- const sessionPath = join(logsDir, targetSession);
313
- const logFiles = this.getLogFilesFromDirectory(sessionPath);
314
- mostRecentSession = {
315
- sessionDate: targetSession,
316
- logFiles,
317
- totalFiles: logFiles.length,
318
- sessionPath
319
- };
320
- }
321
- }
322
- catch (error) {
323
- // Directory read error
324
- }
325
- const recommendedLogs = [];
326
- if (mostRecentSession) {
327
- if (mostRecentSession.logFiles.length === 0) {
328
- recommendedLogs.push(`Session ${mostRecentSession.sessionDate} has no log files`);
329
- }
330
- else {
331
- recommendedLogs.push(`Most recent session: ${mostRecentSession.sessionDate}`);
332
- recommendedLogs.push(...mostRecentSession.logFiles.map(f => `${f.processType}: ${f.fileName} (${f.lineCount || 0} lines, ${Math.round(f.size / 1024)}KB)`));
333
- }
334
- }
335
- else if (sessions.length === 0) {
336
- recommendedLogs.push('No organized log sessions found. Logs will be created in logs/YYYY-MM-DD/ when you start dev servers.');
337
- }
338
- else {
339
- recommendedLogs.push('No logs found in recent sessions. Start dev servers to generate logs.');
340
- }
174
+ catch (err) {
341
175
  return {
342
- status: sessions.length > 0 ? 'success' : 'no_sessions_found',
343
- mostRecentSession,
344
- availableSessions: sessions,
345
- recommendedLogs,
346
- totalSessions: sessions.length
176
+ success: false,
177
+ message: `Failed to stop Amplify sandbox: ${err instanceof Error ? err.message : String(err)}`
347
178
  };
348
179
  }
349
- createStructuredLogPath(command, sessionDate, logType) {
350
- const cwd = process.cwd();
351
- const today = new Date().toISOString().split('T')[0];
352
- const targetDate = sessionDate || today;
353
- // Create logs directory if it doesn't exist
354
- const logsDir = join(cwd, 'logs');
355
- const sessionDir = join(logsDir, targetDate);
356
- if (!existsSync(logsDir)) {
357
- mkdirSync(logsDir, { recursive: true });
358
- }
359
- if (!existsSync(sessionDir)) {
360
- mkdirSync(sessionDir, { recursive: true });
361
- }
362
- // Determine log type and create process type subfolder
363
- const detectedType = logType || this.detectProcessType(command);
364
- const processTypeDir = join(sessionDir, detectedType);
365
- if (!existsSync(processTypeDir)) {
366
- mkdirSync(processTypeDir, { recursive: true });
367
- }
368
- // Generate standardized filename
369
- const standardizedName = this.generateStandardizedLogName(detectedType, command);
370
- return join(processTypeDir, standardizedName);
180
+ }
181
+ // Tail sandbox logs
182
+ function tailSandboxLogs(lines = 50) {
183
+ const logFile = getLogFilePath();
184
+ if (!existsSync(logFile)) {
185
+ return {
186
+ success: false,
187
+ message: "No log file found. Start the sandbox first with dev_start_sandbox."
188
+ };
371
189
  }
372
- generateStandardizedLogName(logType, command) {
373
- // Standardized naming conventions without redundant prefixes
374
- const timestamp = new Date().toTimeString().split(' ')[0].replace(/:/g, '-');
375
- switch (logType) {
376
- case 'frontend':
377
- if (command.includes('next'))
378
- return `nextjs-${timestamp}.log`;
379
- if (command.includes('vite'))
380
- return `vite-${timestamp}.log`;
381
- if (command.includes('react'))
382
- return `react-${timestamp}.log`;
383
- return `dev-${timestamp}.log`;
384
- case 'backend':
385
- if (command.includes('express'))
386
- return `express-${timestamp}.log`;
387
- if (command.includes('fastify'))
388
- return `fastify-${timestamp}.log`;
389
- if (command.includes('node'))
390
- return `node-${timestamp}.log`;
391
- return `api-${timestamp}.log`;
392
- case 'amplify':
393
- if (command.includes('sandbox'))
394
- return `sandbox-${timestamp}.log`;
395
- if (command.includes('deploy'))
396
- return `deploy-${timestamp}.log`;
397
- return `dev-${timestamp}.log`;
398
- default:
399
- // Extract first word of command for custom processes
400
- const cmdName = command.split(' ')[0].replace(/[^a-zA-Z0-9]/g, '');
401
- return `${cmdName}-${timestamp}.log`;
402
- }
190
+ try {
191
+ const content = readFileSync(logFile, "utf-8");
192
+ const allLines = content.split("\n").filter(line => line.trim());
193
+ const tailLines = allLines.slice(-lines);
194
+ return {
195
+ success: true,
196
+ logs: tailLines.join("\n")
197
+ };
403
198
  }
404
- shouldUseStructuredLogging(args) {
405
- // Use structured logging by default unless explicitly disabled
406
- // or if legacy outputFile is provided without structuredLogging flag
407
- if (args.structuredLogging === false)
408
- return false;
409
- if (args.outputFile && args.structuredLogging !== true)
410
- return false;
411
- return true;
199
+ catch (err) {
200
+ return {
201
+ success: false,
202
+ message: `Failed to read logs: ${err instanceof Error ? err.message : String(err)}`
203
+ };
412
204
  }
413
- cleanupOldLogs(retentionDays = 3) {
414
- try {
415
- const cwd = process.cwd();
416
- const logsDir = join(cwd, 'logs');
417
- // Skip if logs directory doesn't exist
418
- if (!existsSync(logsDir)) {
419
- return;
205
+ }
206
+ // Initialize MCP server
207
+ const server = new Server({
208
+ name: "mcp-dev-logger",
209
+ version: "3.0.0",
210
+ }, {
211
+ capabilities: {
212
+ tools: {},
213
+ },
214
+ });
215
+ // List available tools
216
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
217
+ const tools = [
218
+ {
219
+ name: "dev_start_sandbox",
220
+ description: "Start Amplify Gen2 sandbox with streaming function logs. Runs 'npx ampx sandbox --stream-function-logs' and logs to resources/sandbox.log (overwrites previous logs).",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: {},
224
+ required: []
225
+ }
226
+ },
227
+ {
228
+ name: "dev_stop_sandbox",
229
+ description: "Stop the running Amplify sandbox process.",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {},
233
+ required: []
420
234
  }
421
- const now = new Date();
422
- const cutoffTime = now.getTime() - (retentionDays * 24 * 60 * 60 * 1000);
423
- // Read all entries in logs directory
424
- const entries = readdirSync(logsDir);
425
- for (const entry of entries) {
426
- // Check if it's a date folder (YYYY-MM-DD format)
427
- if (/^\d{4}-\d{2}-\d{2}$/.test(entry)) {
428
- const entryPath = join(logsDir, entry);
429
- const stats = statSync(entryPath);
430
- if (stats.isDirectory()) {
431
- // Parse the date from folder name
432
- const folderDate = new Date(entry + 'T00:00:00');
433
- // If folder is older than retention period, remove it
434
- if (folderDate.getTime() < cutoffTime) {
435
- try {
436
- rmSync(entryPath, { recursive: true, force: true });
437
- console.error(`Cleaned up old log directory: ${entry}`);
438
- }
439
- catch (error) {
440
- console.error(`Failed to remove old log directory ${entry}:`, error);
441
- }
442
- }
235
+ },
236
+ {
237
+ name: "dev_tail_sandbox_logs",
238
+ description: "Get the last N lines from the sandbox log file.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ lines: {
243
+ type: "number",
244
+ description: "Number of lines to return (default: 50)"
443
245
  }
444
- }
246
+ },
247
+ required: []
445
248
  }
446
249
  }
447
- catch (error) {
448
- // Log cleanup failures shouldn't break the main functionality
449
- console.error('Log cleanup error:', error);
450
- }
451
- }
452
- async killAllProcesses() {
453
- for (const [id, info] of this.activeServers) {
454
- try {
455
- // Close browser if running
456
- if (info.browser) {
457
- await info.browser.close();
458
- }
459
- if (info.process) {
460
- // Remove all event listeners to prevent memory leaks
461
- info.process.stdout?.removeAllListeners('data');
462
- info.process.stderr?.removeAllListeners('data');
463
- info.process.removeAllListeners('exit');
464
- info.process.removeAllListeners('error');
465
- // Kill the process
466
- if (info.pid) {
467
- // Kill the process group (negative PID) to kill all children
468
- process.kill(-info.pid, 'SIGTERM');
469
- }
470
- }
250
+ ];
251
+ return { tools };
252
+ });
253
+ // Handle tool calls
254
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
255
+ const { name, arguments: args } = request.params;
256
+ try {
257
+ switch (name) {
258
+ case "dev_start_sandbox": {
259
+ const result = await startSandbox();
260
+ return {
261
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
262
+ };
471
263
  }
472
- catch (error) {
473
- // Process might already be dead, try individual PID
474
- try {
475
- if (info.pid) {
476
- process.kill(info.pid, 'SIGTERM');
477
- }
478
- }
479
- catch (fallbackError) {
480
- // Ignore - process is definitely dead
481
- }
264
+ case "dev_stop_sandbox": {
265
+ const result = stopSandbox();
266
+ return {
267
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
268
+ };
482
269
  }
483
- }
484
- this.activeServers.clear();
485
- }
486
- setupHandlers() {
487
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
488
- const tools = [
489
- {
490
- name: "dev_start_log_streaming",
491
- description: "Start development server with organized logging (logs/YYYY-MM-DD/). Supports structured sessions and standardized file naming.",
492
- inputSchema: {
493
- type: "object",
494
- properties: {
495
- command: {
496
- type: "string",
497
- description: "Dev command to run (default: npm run dev)"
498
- },
499
- outputFile: {
500
- type: "string",
501
- description: "File to write logs to (DEPRECATED: use structuredLogging instead)"
502
- },
503
- cwd: {
504
- type: "string",
505
- description: "Working directory"
506
- },
507
- env: {
508
- type: "object",
509
- description: "Environment variables",
510
- additionalProperties: {
511
- type: "string"
512
- }
513
- },
514
- processId: {
515
- type: "string",
516
- description: "Custom process ID (auto-generated if not provided)"
517
- },
518
- structuredLogging: {
519
- type: "boolean",
520
- description: "Use organized logging with date-based sessions (default: true)"
521
- },
522
- sessionDate: {
523
- type: "string",
524
- description: "Session date for organized logging (YYYY-MM-DD, defaults to today)"
525
- },
526
- logType: {
527
- type: "string",
528
- enum: ["frontend", "backend", "amplify", "custom"],
529
- description: "Process type for standardized naming (auto-detected if not provided)"
530
- }
531
- }
532
- }
533
- },
534
- {
535
- name: "dev_get_processes",
536
- description: "Get information about running development processes with optional formatting",
537
- inputSchema: {
538
- type: "object",
539
- properties: {
540
- processId: {
541
- type: "string",
542
- description: "Process ID to get info for (if not provided, shows all processes)"
543
- },
544
- format: {
545
- type: "string",
546
- enum: ["raw", "formatted"],
547
- description: "Output format: raw (milliseconds) or formatted (hours/minutes/seconds) - default: formatted"
548
- }
549
- }
550
- }
551
- },
552
- {
553
- name: "dev_stop_log_streaming",
554
- description: "Stop development server(s) and logging",
555
- inputSchema: {
556
- type: "object",
557
- properties: {
558
- processId: {
559
- type: "string",
560
- description: "Process ID to stop (if not provided, stops all processes)"
561
- }
562
- }
563
- }
564
- },
565
- {
566
- name: "dev_restart_log_streaming",
567
- description: "Restart a specific development server",
568
- inputSchema: {
569
- type: "object",
570
- properties: {
571
- processId: {
572
- type: "string",
573
- description: "Process ID to restart"
574
- },
575
- clearLogs: {
576
- type: "boolean",
577
- description: "Clear logs on restart (default: false)"
578
- }
579
- },
580
- required: ["processId"]
581
- }
582
- },
583
- {
584
- name: "dev_tail_logs",
585
- description: "Get last N lines from log file",
586
- inputSchema: {
587
- type: "object",
588
- properties: {
589
- processId: {
590
- type: "string",
591
- description: "Process ID to tail logs from (if not provided, tries to find single process)"
592
- },
593
- lines: {
594
- type: "number",
595
- description: "Number of lines to return (default: 50)"
596
- },
597
- filter: {
598
- type: "string",
599
- description: "Grep pattern to filter logs"
600
- }
601
- }
602
- }
603
- },
604
- {
605
- name: "dev_clear_logs",
606
- description: "Clear the log file",
607
- inputSchema: {
608
- type: "object",
609
- properties: {
610
- processId: {
611
- type: "string",
612
- description: "Process ID to clear logs for (if not provided, tries to find single process)"
613
- },
614
- backup: {
615
- type: "boolean",
616
- description: "Backup logs before clearing (default: false)"
617
- }
618
- }
619
- }
620
- },
621
- {
622
- name: "dev_check_running_processes",
623
- description: "Check for running development processes that might conflict with new servers",
624
- inputSchema: {
625
- type: "object",
626
- properties: {
627
- processType: {
628
- type: "string",
629
- enum: ["frontend", "backend", "amplify", "custom"],
630
- description: "Type of process to check for conflicts"
631
- },
632
- port: {
633
- type: "number",
634
- description: "Specific port to check"
635
- }
636
- }
637
- }
638
- },
639
- {
640
- name: "dev_discover_logs",
641
- description: "Auto-discover available log files and sessions in organized directory structure",
642
- inputSchema: {
643
- type: "object",
644
- properties: {
645
- sessionDate: {
646
- type: "string",
647
- description: "Specific session date (YYYY-MM-DD) to discover logs for (defaults to most recent)"
648
- }
649
- }
650
- }
651
- },
652
- {
653
- name: "dev_launch_test_browser",
654
- description: "Launch a TEST BROWSER for students to interact with. All console logs from this browser are captured automatically. Students should use THIS browser (not their regular browser) to test their app.",
655
- inputSchema: {
656
- type: "object",
657
- properties: {
658
- processId: {
659
- type: "string",
660
- description: "Process ID to attach browser console to"
661
- },
662
- browserUrl: {
663
- type: "string",
664
- description: "URL to open in test browser (default: http://localhost:3000)"
665
- },
666
- teachingMode: {
667
- type: "boolean",
668
- description: "Enable teaching mode with DevTools open and slower actions (default: true)"
669
- },
670
- viewport: {
671
- type: "object",
672
- properties: {
673
- width: { type: "number", default: 1280 },
674
- height: { type: "number", default: 800 }
675
- },
676
- description: "Browser window size"
677
- },
678
- highlightErrors: {
679
- type: "boolean",
680
- description: "Add visual indicator when console errors occur (default: true)"
681
- }
682
- }
683
- }
684
- },
685
- {
686
- name: "dev_stop_browser_console",
687
- description: "Stop browser console capture for a specific process",
688
- inputSchema: {
689
- type: "object",
690
- properties: {
691
- processId: {
692
- type: "string",
693
- description: "Process ID to stop browser console capture for"
694
- }
695
- }
696
- }
697
- },
698
- {
699
- name: "dev_start_frontend_with_browser",
700
- description: "🚀 ONE-CLICK STUDENT WORKFLOW: Start frontend server AND automatically launch test browser when ready. Perfect for teaching environments - reduces setup from 3 commands to 1. Browser launches with DevTools open, error highlighting, and all console logs captured.",
701
- inputSchema: {
702
- type: "object",
703
- properties: {
704
- command: {
705
- type: "string",
706
- description: "Dev command to run (default: npm run dev)"
707
- },
708
- port: {
709
- type: "number",
710
- description: "Port to wait for (default: auto-detect from output)"
711
- },
712
- waitTimeout: {
713
- type: "number",
714
- description: "Max time to wait for server in ms (default: 30000)"
715
- },
716
- browserDelay: {
717
- type: "number",
718
- description: "Delay after server ready in ms (default: 1000)"
719
- },
720
- teachingMode: {
721
- type: "boolean",
722
- description: "Enable teaching features (default: true)"
723
- },
724
- processId: {
725
- type: "string",
726
- description: "Custom process ID (default: frontend-with-browser)"
727
- },
728
- env: {
729
- type: "object",
730
- description: "Environment variables"
731
- },
732
- cwd: {
733
- type: "string",
734
- description: "Working directory"
735
- }
736
- }
737
- }
738
- }
739
- ];
740
- return { tools };
741
- });
742
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
743
- try {
744
- const { name, arguments: args } = request.params;
745
- switch (name) {
746
- case "dev_start_log_streaming": {
747
- const validatedArgs = StartLogStreamingArgsSchema.parse(args);
748
- const command = validatedArgs.command || "npm run dev";
749
- const cwd = resolve(validatedArgs.cwd || process.cwd());
750
- // Determine output file path using structured logging or legacy approach
751
- let outputFile;
752
- if (this.shouldUseStructuredLogging(validatedArgs)) {
753
- outputFile = this.createStructuredLogPath(command, validatedArgs.sessionDate, validatedArgs.logType);
754
- // Clean up old logs when using structured logging (keeps last 3 days)
755
- this.cleanupOldLogs(3);
756
- }
757
- else {
758
- outputFile = resolve(validatedArgs.outputFile || "dev-server-logs.txt");
759
- }
760
- // Generate or use provided process ID
761
- let processId = validatedArgs.processId || this.generateProcessId(command, outputFile);
762
- // Ensure unique process ID
763
- let counter = 1;
764
- const originalProcessId = processId;
765
- while (this.activeServers.has(processId)) {
766
- processId = `${originalProcessId}-${counter}`;
767
- counter++;
768
- }
769
- // Parse command into program and args
770
- const [program, ...cmdArgs] = command.split(' ');
771
- // Create or clear the output file
772
- writeFileSync(outputFile, `[${new Date().toISOString()}] Starting: ${command} (Process ID: ${processId})\n`);
773
- try {
774
- // Spawn the process (detached to avoid signal propagation)
775
- const devProcess = spawn(program, cmdArgs, {
776
- cwd: cwd,
777
- env: { ...process.env, ...validatedArgs.env },
778
- detached: true,
779
- stdio: ['ignore', 'pipe', 'pipe']
780
- });
781
- const serverInfo = {
782
- process: devProcess,
783
- command: command,
784
- cwd: cwd,
785
- outputFile: outputFile,
786
- startTime: new Date(),
787
- pid: devProcess.pid,
788
- processId: processId
789
- };
790
- this.activeServers.set(processId, serverInfo);
791
- // Stream stdout to file
792
- devProcess.stdout?.on('data', (data) => {
793
- const timestamp = new Date().toISOString();
794
- const logEntry = `[${timestamp}] [${processId}] ${data}`;
795
- appendFileSync(outputFile, logEntry);
796
- });
797
- // Stream stderr to file
798
- devProcess.stderr?.on('data', (data) => {
799
- const timestamp = new Date().toISOString();
800
- const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
801
- appendFileSync(outputFile, logEntry);
802
- });
803
- // Handle process exit
804
- devProcess.on('exit', (code, signal) => {
805
- const timestamp = new Date().toISOString();
806
- const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
807
- appendFileSync(outputFile, exitMessage);
808
- this.activeServers.delete(processId);
809
- this.saveState();
810
- });
811
- // Handle process errors
812
- devProcess.on('error', (error) => {
813
- const timestamp = new Date().toISOString();
814
- const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
815
- appendFileSync(outputFile, errorMessage);
816
- });
817
- this.saveState();
818
- return {
819
- content: [
820
- {
821
- type: "text",
822
- text: JSON.stringify({
823
- status: "started",
824
- processId: processId,
825
- pid: devProcess.pid,
826
- command: command,
827
- cwd: cwd,
828
- outputFile: outputFile,
829
- message: `Dev server started with process ID: ${processId}. Logs streaming to ${outputFile}`
830
- }, null, 2)
831
- }
832
- ]
833
- };
834
- }
835
- catch (error) {
836
- throw new Error(`Failed to start dev server: ${error instanceof Error ? error.message : String(error)}`);
837
- }
838
- }
839
- case "dev_get_processes": {
840
- const validatedArgs = GetProcessesArgsSchema.parse(args);
841
- const format = validatedArgs.format || "formatted";
842
- if (validatedArgs.processId) {
843
- // Get info for specific process
844
- const serverInfo = this.activeServers.get(validatedArgs.processId);
845
- if (!serverInfo) {
846
- return {
847
- content: [
848
- {
849
- type: "text",
850
- text: JSON.stringify({
851
- status: "not_found",
852
- message: `Process with ID '${validatedArgs.processId}' not found`
853
- }, null, 2)
854
- }
855
- ]
856
- };
857
- }
858
- const uptime = new Date().getTime() - serverInfo.startTime.getTime();
859
- const processInfo = {
860
- processId: serverInfo.processId,
861
- pid: serverInfo.pid,
862
- command: serverInfo.command,
863
- cwd: serverInfo.cwd,
864
- outputFile: serverInfo.outputFile,
865
- startTime: serverInfo.startTime,
866
- uptime: format === "raw" ? uptime : {
867
- hours: Math.floor(uptime / 3600000),
868
- minutes: Math.floor((uptime % 3600000) / 60000),
869
- seconds: Math.floor((uptime % 60000) / 1000)
870
- }
871
- };
872
- return {
873
- content: [
874
- {
875
- type: "text",
876
- text: JSON.stringify({
877
- status: "running",
878
- process: processInfo
879
- }, null, 2)
880
- }
881
- ]
882
- };
883
- }
884
- else {
885
- // Get info for all processes
886
- if (this.activeServers.size === 0) {
887
- return {
888
- content: [
889
- {
890
- type: "text",
891
- text: JSON.stringify({
892
- status: "not_running",
893
- message: "No dev servers are currently running"
894
- }, null, 2)
895
- }
896
- ]
897
- };
898
- }
899
- const processes = Array.from(this.activeServers.values()).map(serverInfo => {
900
- const uptime = new Date().getTime() - serverInfo.startTime.getTime();
901
- return {
902
- processId: serverInfo.processId,
903
- pid: serverInfo.pid,
904
- command: serverInfo.command,
905
- cwd: serverInfo.cwd,
906
- outputFile: serverInfo.outputFile,
907
- startTime: serverInfo.startTime,
908
- uptime: format === "raw" ? uptime : {
909
- hours: Math.floor(uptime / 3600000),
910
- minutes: Math.floor((uptime % 3600000) / 60000),
911
- seconds: Math.floor((uptime % 60000) / 1000)
912
- }
913
- };
914
- });
915
- return {
916
- content: [
917
- {
918
- type: "text",
919
- text: JSON.stringify({
920
- status: "running",
921
- totalProcesses: processes.length,
922
- processes: processes
923
- }, null, 2)
924
- }
925
- ]
926
- };
927
- }
928
- }
929
- case "dev_stop_log_streaming": {
930
- const validatedArgs = StopLogStreamingArgsSchema.parse(args);
931
- if (validatedArgs.processId) {
932
- // Stop specific process
933
- const serverInfo = this.activeServers.get(validatedArgs.processId);
934
- if (!serverInfo) {
935
- return {
936
- content: [
937
- {
938
- type: "text",
939
- text: JSON.stringify({
940
- status: "not_found",
941
- message: `Process with ID '${validatedArgs.processId}' not found`
942
- }, null, 2)
943
- }
944
- ]
945
- };
946
- }
947
- try {
948
- // Close browser if running
949
- if (serverInfo.browser) {
950
- await serverInfo.browser.close();
951
- }
952
- // Remove all event listeners to prevent memory leaks
953
- if (serverInfo.process) {
954
- serverInfo.process.stdout?.removeAllListeners('data');
955
- serverInfo.process.stderr?.removeAllListeners('data');
956
- serverInfo.process.removeAllListeners('exit');
957
- serverInfo.process.removeAllListeners('error');
958
- }
959
- if (serverInfo.pid) {
960
- process.kill(serverInfo.pid, 'SIGTERM');
961
- }
962
- this.activeServers.delete(validatedArgs.processId);
963
- this.saveState();
964
- return {
965
- content: [
966
- {
967
- type: "text",
968
- text: JSON.stringify({
969
- status: "stopped",
970
- processId: validatedArgs.processId,
971
- message: `Process '${validatedArgs.processId}' stopped successfully`
972
- }, null, 2)
973
- }
974
- ]
975
- };
976
- }
977
- catch (error) {
978
- throw new Error(`Failed to stop process '${validatedArgs.processId}': ${error instanceof Error ? error.message : String(error)}`);
979
- }
980
- }
981
- else {
982
- // Stop all processes
983
- if (this.activeServers.size === 0) {
984
- return {
985
- content: [
986
- {
987
- type: "text",
988
- text: JSON.stringify({
989
- status: "not_running",
990
- message: "No dev servers are currently running"
991
- }, null, 2)
992
- }
993
- ]
994
- };
995
- }
996
- const stoppedProcesses = [];
997
- for (const [processId, serverInfo] of this.activeServers) {
998
- try {
999
- // Close browser if running
1000
- if (serverInfo.browser) {
1001
- await serverInfo.browser.close();
1002
- }
1003
- // Remove all event listeners to prevent memory leaks
1004
- if (serverInfo.process) {
1005
- serverInfo.process.stdout?.removeAllListeners('data');
1006
- serverInfo.process.stderr?.removeAllListeners('data');
1007
- serverInfo.process.removeAllListeners('exit');
1008
- serverInfo.process.removeAllListeners('error');
1009
- }
1010
- if (serverInfo.pid) {
1011
- process.kill(serverInfo.pid, 'SIGTERM');
1012
- }
1013
- stoppedProcesses.push(processId);
1014
- }
1015
- catch (error) {
1016
- // Continue with other processes even if one fails
1017
- }
1018
- }
1019
- this.activeServers.clear();
1020
- this.saveState();
1021
- return {
1022
- content: [
1023
- {
1024
- type: "text",
1025
- text: JSON.stringify({
1026
- status: "stopped",
1027
- stoppedProcesses: stoppedProcesses,
1028
- message: `Stopped ${stoppedProcesses.length} processes`
1029
- }, null, 2)
1030
- }
1031
- ]
1032
- };
1033
- }
1034
- }
1035
- case "dev_restart_log_streaming": {
1036
- const validatedArgs = RestartLogStreamingArgsSchema.parse(args);
1037
- const processId = validatedArgs.processId;
1038
- const serverInfo = this.activeServers.get(processId);
1039
- if (!serverInfo) {
1040
- return {
1041
- content: [
1042
- {
1043
- type: "text",
1044
- text: JSON.stringify({
1045
- status: "not_found",
1046
- message: `Process with ID '${processId}' not found. Use dev_list_processes to see available processes.`
1047
- }, null, 2)
1048
- }
1049
- ]
1050
- };
1051
- }
1052
- // Save current configuration
1053
- const { command, cwd, outputFile } = serverInfo;
1054
- // Stop current process
1055
- try {
1056
- // Remove all event listeners to prevent memory leaks
1057
- if (serverInfo.process) {
1058
- serverInfo.process.stdout?.removeAllListeners('data');
1059
- serverInfo.process.stderr?.removeAllListeners('data');
1060
- serverInfo.process.removeAllListeners('exit');
1061
- serverInfo.process.removeAllListeners('error');
1062
- }
1063
- if (serverInfo.pid) {
1064
- process.kill(serverInfo.pid, 'SIGTERM');
1065
- }
1066
- }
1067
- catch (error) {
1068
- // Process might already be dead
1069
- }
1070
- // Clear logs if requested
1071
- if (validatedArgs.clearLogs && existsSync(outputFile)) {
1072
- writeFileSync(outputFile, '');
1073
- }
1074
- // Remove the old server info to ensure complete cleanup
1075
- this.activeServers.delete(processId);
1076
- // Wait a moment for process to die
1077
- await new Promise(resolve => setTimeout(resolve, 1000));
1078
- // Start new process
1079
- const [program, ...cmdArgs] = command.split(' ');
1080
- const restartMessage = `[${new Date().toISOString()}] Restarting: ${command} (Process ID: ${processId})\n`;
1081
- if (validatedArgs.clearLogs) {
1082
- writeFileSync(outputFile, restartMessage);
1083
- }
1084
- else {
1085
- appendFileSync(outputFile, restartMessage);
1086
- }
1087
- const devProcess = spawn(program, cmdArgs, {
1088
- cwd: cwd,
1089
- env: process.env,
1090
- detached: true,
1091
- stdio: ['ignore', 'pipe', 'pipe']
1092
- });
1093
- const newServerInfo = {
1094
- process: devProcess,
1095
- command: command,
1096
- cwd: cwd,
1097
- outputFile: outputFile,
1098
- startTime: new Date(),
1099
- pid: devProcess.pid,
1100
- processId: processId
1101
- };
1102
- this.activeServers.set(processId, newServerInfo);
1103
- // Stream stdout to file
1104
- devProcess.stdout?.on('data', (data) => {
1105
- const timestamp = new Date().toISOString();
1106
- const logEntry = `[${timestamp}] [${processId}] ${data}`;
1107
- appendFileSync(outputFile, logEntry);
1108
- });
1109
- // Stream stderr to file
1110
- devProcess.stderr?.on('data', (data) => {
1111
- const timestamp = new Date().toISOString();
1112
- const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
1113
- appendFileSync(outputFile, logEntry);
1114
- });
1115
- // Handle process exit
1116
- devProcess.on('exit', (code, signal) => {
1117
- const timestamp = new Date().toISOString();
1118
- const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
1119
- appendFileSync(outputFile, exitMessage);
1120
- this.activeServers.delete(processId);
1121
- this.saveState();
1122
- });
1123
- // Handle process errors
1124
- devProcess.on('error', (error) => {
1125
- const timestamp = new Date().toISOString();
1126
- const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
1127
- appendFileSync(outputFile, errorMessage);
1128
- });
1129
- this.saveState();
1130
- return {
1131
- content: [
1132
- {
1133
- type: "text",
1134
- text: JSON.stringify({
1135
- status: "restarted",
1136
- processId: processId,
1137
- pid: devProcess.pid,
1138
- command: command,
1139
- outputFile: outputFile,
1140
- message: `Process '${processId}' restarted successfully`
1141
- }, null, 2)
1142
- }
1143
- ]
1144
- };
1145
- }
1146
- case "dev_tail_logs": {
1147
- const validatedArgs = TailLogsArgsSchema.parse(args);
1148
- // Find the process or use the default behavior
1149
- const processId = this.findProcessOrDefault(validatedArgs.processId);
1150
- if (!processId && validatedArgs.processId) {
1151
- return {
1152
- content: [
1153
- {
1154
- type: "text",
1155
- text: JSON.stringify({
1156
- status: "not_found",
1157
- message: `Process with ID '${validatedArgs.processId}' not found`
1158
- }, null, 2)
1159
- }
1160
- ]
1161
- };
1162
- }
1163
- if (!processId && this.activeServers.size > 1) {
1164
- return {
1165
- content: [
1166
- {
1167
- type: "text",
1168
- text: JSON.stringify({
1169
- status: "ambiguous",
1170
- message: "Multiple processes running. Please specify processId or use dev_list_processes to see options.",
1171
- availableProcesses: Array.from(this.activeServers.keys())
1172
- }, null, 2)
1173
- }
1174
- ]
1175
- };
1176
- }
1177
- // Get the output file
1178
- let outputFile;
1179
- if (processId) {
1180
- const serverInfo = this.activeServers.get(processId);
1181
- outputFile = serverInfo.outputFile;
1182
- }
1183
- else {
1184
- outputFile = "dev-server-logs.txt"; // fallback
1185
- }
1186
- if (!existsSync(outputFile)) {
1187
- return {
1188
- content: [
1189
- {
1190
- type: "text",
1191
- text: JSON.stringify({
1192
- status: "error",
1193
- message: `Log file not found: ${outputFile}`
1194
- }, null, 2)
1195
- }
1196
- ]
1197
- };
1198
- }
1199
- try {
1200
- // Read the file
1201
- const content = readFileSync(outputFile, 'utf8');
1202
- const lines = content.split('\n').filter(line => line.trim());
1203
- // Filter if pattern provided
1204
- let filteredLines = lines;
1205
- if (validatedArgs.filter) {
1206
- const pattern = new RegExp(validatedArgs.filter, 'i');
1207
- filteredLines = lines.filter(line => pattern.test(line));
1208
- }
1209
- // Get last N lines
1210
- const numLines = validatedArgs.lines || 50;
1211
- const lastLines = filteredLines.slice(-numLines);
1212
- return {
1213
- content: [
1214
- {
1215
- type: "text",
1216
- text: JSON.stringify({
1217
- status: "success",
1218
- processId: processId || "default",
1219
- file: outputFile,
1220
- totalLines: lines.length,
1221
- filteredLines: filteredLines.length,
1222
- returnedLines: lastLines.length,
1223
- logs: lastLines
1224
- }, null, 2)
1225
- }
1226
- ]
1227
- };
1228
- }
1229
- catch (error) {
1230
- throw new Error(`Failed to read logs: ${error instanceof Error ? error.message : String(error)}`);
1231
- }
1232
- }
1233
- case "dev_clear_logs": {
1234
- const validatedArgs = ClearLogsArgsSchema.parse(args);
1235
- // Find the process or use the default behavior
1236
- const processId = this.findProcessOrDefault(validatedArgs.processId);
1237
- if (!processId && validatedArgs.processId) {
1238
- return {
1239
- content: [
1240
- {
1241
- type: "text",
1242
- text: JSON.stringify({
1243
- status: "not_found",
1244
- message: `Process with ID '${validatedArgs.processId}' not found`
1245
- }, null, 2)
1246
- }
1247
- ]
1248
- };
1249
- }
1250
- if (!processId && this.activeServers.size > 1) {
1251
- return {
1252
- content: [
1253
- {
1254
- type: "text",
1255
- text: JSON.stringify({
1256
- status: "ambiguous",
1257
- message: "Multiple processes running. Please specify processId or use dev_list_processes to see options.",
1258
- availableProcesses: Array.from(this.activeServers.keys())
1259
- }, null, 2)
1260
- }
1261
- ]
1262
- };
1263
- }
1264
- // Get the output file
1265
- let outputFile;
1266
- if (processId) {
1267
- const serverInfo = this.activeServers.get(processId);
1268
- outputFile = serverInfo.outputFile;
1269
- }
1270
- else {
1271
- outputFile = "dev-server-logs.txt"; // fallback
1272
- }
1273
- if (!existsSync(outputFile)) {
1274
- return {
1275
- content: [
1276
- {
1277
- type: "text",
1278
- text: JSON.stringify({
1279
- status: "error",
1280
- message: `Log file not found: ${outputFile}`
1281
- }, null, 2)
1282
- }
1283
- ]
1284
- };
1285
- }
1286
- try {
1287
- // Backup if requested
1288
- if (validatedArgs.backup) {
1289
- const backupFile = `${outputFile}.${new Date().getTime()}.backup`;
1290
- copyFileSync(outputFile, backupFile);
1291
- }
1292
- // Clear the file
1293
- writeFileSync(outputFile, '');
1294
- return {
1295
- content: [
1296
- {
1297
- type: "text",
1298
- text: JSON.stringify({
1299
- status: "success",
1300
- processId: processId || "default",
1301
- message: `Log file cleared: ${outputFile}`,
1302
- backup: validatedArgs.backup ? "Created backup" : "No backup created"
1303
- }, null, 2)
1304
- }
1305
- ]
1306
- };
1307
- }
1308
- catch (error) {
1309
- throw new Error(`Failed to clear logs: ${error instanceof Error ? error.message : String(error)}`);
1310
- }
1311
- }
1312
- case "dev_check_running_processes": {
1313
- const validatedArgs = CheckRunningProcessesArgsSchema.parse(args);
1314
- try {
1315
- const result = await this.checkRunningProcesses(validatedArgs.processType, validatedArgs.port);
1316
- return {
1317
- content: [
1318
- {
1319
- type: "text",
1320
- text: JSON.stringify(result, null, 2)
1321
- }
1322
- ]
1323
- };
1324
- }
1325
- catch (error) {
1326
- throw new Error(`Failed to check running processes: ${error instanceof Error ? error.message : String(error)}`);
1327
- }
1328
- }
1329
- case "dev_discover_logs": {
1330
- const validatedArgs = DiscoverLogsArgsSchema.parse(args);
1331
- try {
1332
- const result = await this.discoverLogs(validatedArgs.sessionDate);
1333
- return {
1334
- content: [
1335
- {
1336
- type: "text",
1337
- text: JSON.stringify(result, null, 2)
1338
- }
1339
- ]
1340
- };
1341
- }
1342
- catch (error) {
1343
- throw new Error(`Failed to discover logs: ${error instanceof Error ? error.message : String(error)}`);
1344
- }
1345
- }
1346
- case "dev_launch_test_browser": {
1347
- const validatedArgs = z.object({
1348
- processId: z.string().optional(),
1349
- browserUrl: z.string().optional(),
1350
- teachingMode: z.boolean().optional(),
1351
- viewport: z.object({
1352
- width: z.number().default(1280),
1353
- height: z.number().default(800)
1354
- }).optional(),
1355
- highlightErrors: z.boolean().optional()
1356
- }).parse(args);
1357
- try {
1358
- // Find the process to attach to
1359
- const processId = this.findProcessOrDefault(validatedArgs.processId);
1360
- if (!processId) {
1361
- return {
1362
- content: [
1363
- {
1364
- type: "text",
1365
- text: JSON.stringify({
1366
- status: "error",
1367
- message: "No dev server process found. Start a dev server first with dev_start_log_streaming"
1368
- }, null, 2)
1369
- }
1370
- ]
1371
- };
1372
- }
1373
- const serverInfo = this.activeServers.get(processId);
1374
- // Check if browser already running for this process
1375
- if (serverInfo.browser) {
1376
- return {
1377
- content: [
1378
- {
1379
- type: "text",
1380
- text: JSON.stringify({
1381
- status: "already_running",
1382
- message: `Browser console capture already active for process '${processId}'`
1383
- }, null, 2)
1384
- }
1385
- ]
1386
- };
1387
- }
1388
- // Launch browser with student-friendly defaults
1389
- const teachingMode = validatedArgs.teachingMode !== false;
1390
- const viewport = validatedArgs.viewport || { width: 1280, height: 800 };
1391
- const browser = await chromium.launch({
1392
- headless: false, // Always visible for students
1393
- slowMo: teachingMode ? 50 : 0, // Slow down actions in teaching mode
1394
- devtools: teachingMode, // Open DevTools in teaching mode
1395
- args: [
1396
- `--window-size=${viewport.width},${viewport.height}`,
1397
- '--window-position=100,100',
1398
- '--disable-features=RendererCodeIntegrity' // Prevent some security warnings
1399
- ]
1400
- });
1401
- const context = await browser.newContext({
1402
- viewport: viewport,
1403
- ignoreHTTPSErrors: true // Common in dev environments
1404
- });
1405
- const page = await context.newPage();
1406
- // Add page title to identify this as the test browser
1407
- await page.evaluate(() => {
1408
- // @ts-ignore - browser context
1409
- document.title = '🧪 TEST BROWSER - Console Logs Captured';
1410
- });
1411
- // Set up console event handler with student-friendly features
1412
- const highlightErrors = validatedArgs.highlightErrors !== false;
1413
- page.on('console', (msg) => {
1414
- const msgType = msg.type();
1415
- const timestamp = new Date().toISOString();
1416
- const args = msg.args();
1417
- // Format console message
1418
- let messageText = msg.text();
1419
- // Visual feedback for errors in teaching mode
1420
- if (highlightErrors && msgType === 'error') {
1421
- page.evaluate(() => {
1422
- // @ts-ignore - browser context
1423
- const flash = document.createElement('div');
1424
- flash.style.cssText = `
1425
- position: fixed;
1426
- top: 0;
1427
- left: 0;
1428
- right: 0;
1429
- bottom: 0;
1430
- border: 5px solid red;
1431
- pointer-events: none;
1432
- z-index: 999999;
1433
- animation: flash 0.5s ease-in-out;
1434
- `;
1435
- flash.innerHTML = `
1436
- <div style="
1437
- background: red;
1438
- color: white;
1439
- padding: 10px;
1440
- position: absolute;
1441
- top: 0;
1442
- left: 0;
1443
- right: 0;
1444
- text-align: center;
1445
- font-family: monospace;
1446
- ">⚠️ Console Error Detected - Check Logs!</div>
1447
- `;
1448
- // @ts-ignore - browser context
1449
- document.body.appendChild(flash);
1450
- setTimeout(() => flash.remove(), 2000);
1451
- }).catch(() => { });
1452
- }
1453
- // Try to get better formatting for objects
1454
- if (args.length > 0) {
1455
- Promise.all(args.map(arg => arg.jsonValue().catch(() => arg.toString())))
1456
- .then(values => {
1457
- const formattedMsg = values.map(v => typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v)).join(' ');
1458
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType.toUpperCase()}] ${formattedMsg}\n`;
1459
- appendFileSync(serverInfo.outputFile, logEntry);
1460
- // Also log to terminal for immediate feedback
1461
- if (msgType === 'error' || msgType === 'warning') {
1462
- console.error(`[STUDENT BROWSER] ${msgType === 'warning' ? 'WARN' : msgType.toUpperCase()}: ${formattedMsg}`);
1463
- }
1464
- })
1465
- .catch(() => {
1466
- // Fallback to simple text
1467
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType.toUpperCase()}] ${messageText}\n`;
1468
- appendFileSync(serverInfo.outputFile, logEntry);
1469
- });
1470
- }
1471
- else {
1472
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType.toUpperCase()}] ${messageText}\n`;
1473
- appendFileSync(serverInfo.outputFile, logEntry);
1474
- }
1475
- });
1476
- // Handle page errors (including hydration errors)
1477
- page.on('pageerror', (error) => {
1478
- const timestamp = new Date().toISOString();
1479
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] [PAGE ERROR] ${error.message}\n${error.stack}\n`;
1480
- appendFileSync(serverInfo.outputFile, logEntry);
1481
- // Visual feedback for page errors
1482
- page.evaluate(() => {
1483
- // @ts-ignore - browser context
1484
- const errorBanner = document.createElement('div');
1485
- errorBanner.style.cssText = `
1486
- position: fixed;
1487
- top: 40px;
1488
- left: 50%;
1489
- transform: translateX(-50%);
1490
- background: #dc2626;
1491
- color: white;
1492
- padding: 12px 24px;
1493
- border-radius: 6px;
1494
- font-family: monospace;
1495
- font-size: 14px;
1496
- z-index: 999999;
1497
- box-shadow: 0 4px 6px rgba(0,0,0,0.3);
1498
- animation: slideDown 0.3s ease-out;
1499
- `;
1500
- errorBanner.textContent = '⚠️ JavaScript Error - Check Console & Logs';
1501
- // @ts-ignore - browser context
1502
- const style = document.createElement('style');
1503
- style.textContent = `
1504
- @keyframes slideDown {
1505
- from { transform: translate(-50%, -100%); opacity: 0; }
1506
- to { transform: translate(-50%, 0); opacity: 1; }
1507
- }
1508
- `;
1509
- // @ts-ignore - browser context
1510
- document.head.appendChild(style);
1511
- // @ts-ignore - browser context
1512
- document.body.appendChild(errorBanner);
1513
- setTimeout(() => errorBanner.remove(), 5000);
1514
- }).catch(() => { });
1515
- });
1516
- // Handle unhandled promise rejections
1517
- await page.addInitScript(() => {
1518
- // @ts-ignore - browser context
1519
- window.addEventListener('unhandledrejection', (event) => {
1520
- console.error('Unhandled Promise Rejection:', event.reason);
1521
- });
1522
- });
1523
- // Handle network response errors
1524
- page.on('response', (response) => {
1525
- if (response.status() >= 400) {
1526
- const timestamp = new Date().toISOString();
1527
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NETWORK ERROR] ${response.status()} ${response.statusText()} - ${response.url()}\n`;
1528
- appendFileSync(serverInfo.outputFile, logEntry);
1529
- }
1530
- });
1531
- // Navigate to URL
1532
- const browserUrl = validatedArgs.browserUrl || "http://localhost:3000";
1533
- try {
1534
- await page.goto(browserUrl, { waitUntil: 'domcontentloaded' });
1535
- // Add student instruction banner (hydration-safe version)
1536
- if (teachingMode) {
1537
- // @ts-ignore
1538
- await page.evaluate(() => {
1539
- function injectTestBanner() {
1540
- // Create a container that won't interfere with React hydration
1541
- const container = document.createElement('div');
1542
- container.id = 'test-browser-banner-container';
1543
- container.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; z-index: 2147483647; pointer-events: none;';
1544
- // Use shadow DOM to isolate the banner completely
1545
- const shadow = container.attachShadow({ mode: 'open' });
1546
- // Create banner in shadow DOM
1547
- const banner = document.createElement('div');
1548
- banner.style.cssText = `
1549
- position: fixed;
1550
- top: 0;
1551
- left: 0;
1552
- right: 0;
1553
- background: #4CAF50;
1554
- color: white;
1555
- padding: 10px;
1556
- text-align: center;
1557
- font-family: monospace;
1558
- font-size: 14px;
1559
- box-shadow: 0 2px 5px rgba(0,0,0,0.2);
1560
- pointer-events: auto;
1561
- `;
1562
- banner.innerHTML = `
1563
- 🧪 TEST BROWSER - All console logs are being captured!
1564
- <button style="
1565
- margin-left: 20px;
1566
- background: transparent;
1567
- border: 1px solid white;
1568
- color: white;
1569
- padding: 2px 10px;
1570
- cursor: pointer;
1571
- ">Hide</button>
1572
- `;
1573
- // Add click handler to hide button
1574
- const button = banner.querySelector('button');
1575
- if (button) {
1576
- button.addEventListener('click', () => {
1577
- container.style.display = 'none';
1578
- });
1579
- }
1580
- shadow.appendChild(banner);
1581
- document.documentElement.appendChild(container);
1582
- }
1583
- // Wait for hydration to complete before injecting
1584
- if (typeof window !== 'undefined') {
1585
- // Check for Next.js hydration
1586
- if (window.__NEXT_HYDRATED) {
1587
- injectTestBanner();
1588
- }
1589
- else {
1590
- // Wait for React to finish hydrating
1591
- let attempts = 0;
1592
- const checkHydration = setInterval(() => {
1593
- attempts++;
1594
- // Check various hydration indicators
1595
- if (window.__NEXT_HYDRATED || window._react_root || document.querySelector('[data-reactroot]') || attempts > 20) {
1596
- clearInterval(checkHydration);
1597
- // Add a small delay to be extra safe
1598
- setTimeout(injectTestBanner, 100);
1599
- }
1600
- }, 250);
1601
- }
1602
- }
1603
- }).catch(() => { });
1604
- }
1605
- }
1606
- catch (error) {
1607
- const timestamp = new Date().toISOString();
1608
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NAVIGATION ERROR] Failed to navigate to ${browserUrl}: ${error}\n`;
1609
- appendFileSync(serverInfo.outputFile, logEntry);
1610
- }
1611
- // Update server info
1612
- serverInfo.browser = browser;
1613
- serverInfo.page = page;
1614
- serverInfo.browserUrl = browserUrl;
1615
- serverInfo.consoleCapture = true;
1616
- // Log browser start
1617
- const timestamp = new Date().toISOString();
1618
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`;
1619
- appendFileSync(serverInfo.outputFile, logEntry);
1620
- return {
1621
- content: [
1622
- {
1623
- type: "text",
1624
- text: JSON.stringify({
1625
- status: "started",
1626
- processId: processId,
1627
- browserUrl: browserUrl,
1628
- teachingMode: teachingMode,
1629
- message: `🧪 TEST BROWSER LAUNCHED!\n\n` +
1630
- `👉 IMPORTANT: Students should use THIS browser window (not their regular browser)\n` +
1631
- `📝 All console logs are being captured to: ${serverInfo.outputFile}\n` +
1632
- `🔍 DevTools is ${teachingMode ? 'OPEN' : 'CLOSED'} for debugging\n` +
1633
- `⚠️ Errors will ${highlightErrors ? 'flash red' : 'not flash'} on screen\n\n` +
1634
- `Students can now interact with the app at: ${browserUrl}`
1635
- }, null, 2)
1636
- }
1637
- ]
1638
- };
1639
- }
1640
- catch (error) {
1641
- throw new Error(`Failed to start browser console capture: ${error instanceof Error ? error.message : String(error)}`);
1642
- }
1643
- }
1644
- case "dev_stop_browser_console": {
1645
- const validatedArgs = z.object({
1646
- processId: z.string().optional()
1647
- }).parse(args);
1648
- try {
1649
- const processId = this.findProcessOrDefault(validatedArgs.processId);
1650
- if (!processId) {
1651
- return {
1652
- content: [
1653
- {
1654
- type: "text",
1655
- text: JSON.stringify({
1656
- status: "error",
1657
- message: "No process found"
1658
- }, null, 2)
1659
- }
1660
- ]
1661
- };
1662
- }
1663
- const serverInfo = this.activeServers.get(processId);
1664
- if (!serverInfo || !serverInfo.browser) {
1665
- return {
1666
- content: [
1667
- {
1668
- type: "text",
1669
- text: JSON.stringify({
1670
- status: "not_running",
1671
- message: `No browser console capture running for process '${processId}'`
1672
- }, null, 2)
1673
- }
1674
- ]
1675
- };
1676
- }
1677
- // Close browser
1678
- await serverInfo.browser.close();
1679
- // Clear browser info
1680
- serverInfo.browser = undefined;
1681
- serverInfo.page = undefined;
1682
- serverInfo.browserUrl = undefined;
1683
- serverInfo.consoleCapture = false;
1684
- // Log browser stop
1685
- const timestamp = new Date().toISOString();
1686
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture stopped\n`;
1687
- appendFileSync(serverInfo.outputFile, logEntry);
1688
- return {
1689
- content: [
1690
- {
1691
- type: "text",
1692
- text: JSON.stringify({
1693
- status: "stopped",
1694
- processId: processId,
1695
- message: `Browser console capture stopped for process '${processId}'`
1696
- }, null, 2)
1697
- }
1698
- ]
1699
- };
1700
- }
1701
- catch (error) {
1702
- throw new Error(`Failed to stop browser console capture: ${error instanceof Error ? error.message : String(error)}`);
1703
- }
1704
- }
1705
- case "dev_start_frontend_with_browser": {
1706
- const validatedArgs = StartFrontendWithBrowserArgsSchema.parse(args);
1707
- // Defaults
1708
- const command = validatedArgs.command || "npm run dev";
1709
- const processId = validatedArgs.processId || "frontend-with-browser";
1710
- const cwd = resolve(validatedArgs.cwd || process.cwd());
1711
- const waitTimeout = validatedArgs.waitTimeout || 30000;
1712
- const browserDelay = validatedArgs.browserDelay || 1000;
1713
- const teachingMode = validatedArgs.teachingMode !== false; // default true
1714
- try {
1715
- // Check if process already exists
1716
- if (this.activeServers.has(processId)) {
1717
- throw new Error(`Process '${processId}' is already running. Stop it first or use a different processId.`);
1718
- }
1719
- // Step 1: Start the dev server
1720
- const outputFile = this.createStructuredLogPath(command, undefined, "frontend");
1721
- this.cleanupOldLogs(3);
1722
- const [program, ...cmdArgs] = command.split(' ');
1723
- // Initial log entry
1724
- writeFileSync(outputFile, `[${new Date().toISOString()}] Starting: ${command} (Process ID: ${processId})\n`);
1725
- appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Starting development server...\n`);
1726
- // Spawn the process
1727
- const devProcess = spawn(program, cmdArgs, {
1728
- cwd: cwd,
1729
- env: { ...process.env, ...validatedArgs.env },
1730
- detached: true,
1731
- stdio: ['ignore', 'pipe', 'pipe']
1732
- });
1733
- const serverInfo = {
1734
- process: devProcess,
1735
- command: command,
1736
- cwd: cwd,
1737
- outputFile: outputFile,
1738
- startTime: new Date(),
1739
- pid: devProcess.pid,
1740
- processId: processId
1741
- };
1742
- this.activeServers.set(processId, serverInfo);
1743
- // Variables for port detection
1744
- let detectedPort = validatedArgs.port || null;
1745
- let serverReady = false;
1746
- let outputBuffer = "";
1747
- // Common port patterns
1748
- const portPatterns = [
1749
- /(?:Local|Listening|ready).*?(\d{4})/i,
1750
- /localhost:(\d{4})/,
1751
- /port\s*:?\s*(\d{4})/i,
1752
- /on\s+http:\/\/localhost:(\d{4})/i,
1753
- /Server running at.*?:(\d{4})/i
1754
- ];
1755
- // Ready indicators
1756
- const readyPatterns = [
1757
- /Local:\s*http:\/\/localhost/i,
1758
- /ready on http:\/\/localhost/i,
1759
- /Server running at/i,
1760
- /Listening on port/i,
1761
- /started server on/i,
1762
- /Ready in \d+ms/i,
1763
- /compiled successfully/i
1764
- ];
1765
- // Create promise for server ready detection
1766
- const serverReadyPromise = new Promise((resolve, reject) => {
1767
- const timeout = setTimeout(() => {
1768
- reject(new Error(`Server did not start within ${waitTimeout}ms`));
1769
- }, waitTimeout);
1770
- const checkOutput = (data) => {
1771
- const text = data.toString();
1772
- outputBuffer += text;
1773
- // Check for port if not detected
1774
- if (!detectedPort) {
1775
- for (const pattern of portPatterns) {
1776
- const match = outputBuffer.match(pattern);
1777
- if (match && match[1]) {
1778
- detectedPort = parseInt(match[1]);
1779
- appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] 🔍 Detected port: ${detectedPort}\n`);
1780
- break;
1781
- }
1782
- }
1783
- }
1784
- // Check for ready state
1785
- if (!serverReady) {
1786
- for (const pattern of readyPatterns) {
1787
- if (pattern.test(outputBuffer)) {
1788
- serverReady = true;
1789
- clearTimeout(timeout);
1790
- // If we have a port, resolve immediately
1791
- if (detectedPort) {
1792
- resolve(detectedPort);
1793
- }
1794
- else {
1795
- // Try common ports
1796
- const commonPorts = [3000, 3001, 5173, 8080, 4200];
1797
- detectedPort = commonPorts[0]; // Default to 3000
1798
- appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ⚠️ Could not detect port, defaulting to ${detectedPort}\n`);
1799
- resolve(detectedPort);
1800
- }
1801
- break;
1802
- }
1803
- }
1804
- }
1805
- };
1806
- // Attach listeners
1807
- devProcess.stdout?.on('data', checkOutput);
1808
- devProcess.stderr?.on('data', checkOutput);
1809
- });
1810
- // Stream stdout to file
1811
- devProcess.stdout?.on('data', (data) => {
1812
- const timestamp = new Date().toISOString();
1813
- const logEntry = `[${timestamp}] [${processId}] ${data}`;
1814
- appendFileSync(outputFile, logEntry);
1815
- });
1816
- // Stream stderr to file
1817
- devProcess.stderr?.on('data', (data) => {
1818
- const timestamp = new Date().toISOString();
1819
- const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
1820
- appendFileSync(outputFile, logEntry);
1821
- });
1822
- // Handle process exit
1823
- devProcess.on('exit', (code, signal) => {
1824
- const timestamp = new Date().toISOString();
1825
- const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
1826
- appendFileSync(outputFile, exitMessage);
1827
- this.activeServers.delete(processId);
1828
- this.saveState();
1829
- });
1830
- // Handle process errors
1831
- devProcess.on('error', (error) => {
1832
- const timestamp = new Date().toISOString();
1833
- const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
1834
- appendFileSync(outputFile, errorMessage);
1835
- });
1836
- this.saveState();
1837
- // Step 2: Wait for server to be ready
1838
- let port;
1839
- try {
1840
- port = await serverReadyPromise;
1841
- appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Server ready on http://localhost:${port}\n`);
1842
- }
1843
- catch (error) {
1844
- // Server didn't start properly, clean up
1845
- const failedServerInfo = this.activeServers.get(processId);
1846
- if (failedServerInfo) {
1847
- try {
1848
- // Close browser if it exists
1849
- if (failedServerInfo.browser) {
1850
- await failedServerInfo.browser.close();
1851
- }
1852
- // Remove all event listeners to prevent memory leaks
1853
- if (failedServerInfo.process) {
1854
- failedServerInfo.process.stdout?.removeAllListeners('data');
1855
- failedServerInfo.process.stderr?.removeAllListeners('data');
1856
- failedServerInfo.process.removeAllListeners('exit');
1857
- failedServerInfo.process.removeAllListeners('error');
1858
- }
1859
- // Kill the process
1860
- if (failedServerInfo.pid) {
1861
- process.kill(failedServerInfo.pid, 'SIGTERM');
1862
- }
1863
- }
1864
- catch (killError) {
1865
- // Ignore errors during cleanup
1866
- }
1867
- this.activeServers.delete(processId);
1868
- this.saveState();
1869
- }
1870
- throw error;
1871
- }
1872
- // Step 3: Wait a bit more for stability
1873
- appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ⏳ Waiting ${browserDelay}ms before launching browser...\n`);
1874
- await new Promise(resolve => setTimeout(resolve, browserDelay));
1875
- // Step 4: Launch the test browser
1876
- try {
1877
- appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Launching test browser...\n`);
1878
- const browser = await chromium.launch({
1879
- headless: false,
1880
- slowMo: teachingMode ? 50 : 0,
1881
- devtools: teachingMode,
1882
- args: ['--window-size=1280,800', '--window-position=100,100']
1883
- });
1884
- const context = await browser.newContext({
1885
- viewport: { width: 1280, height: 800 }
1886
- });
1887
- const page = await context.newPage();
1888
- // Add banner to identify test browser (hydration-safe version)
1889
- // @ts-ignore
1890
- await page.addInitScript(() => {
1891
- function injectTestBanner() {
1892
- // Create a container that won't interfere with React hydration
1893
- const container = document.createElement('div');
1894
- container.id = 'test-browser-banner-container';
1895
- container.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; z-index: 2147483647; pointer-events: none;';
1896
- // Use shadow DOM to isolate the banner completely
1897
- const shadow = container.attachShadow({ mode: 'open' });
1898
- // Create banner in shadow DOM
1899
- const banner = document.createElement('div');
1900
- banner.style.cssText = `
1901
- position: fixed;
1902
- top: 0;
1903
- left: 0;
1904
- right: 0;
1905
- background: #22c55e;
1906
- color: white;
1907
- padding: 8px;
1908
- text-align: center;
1909
- font-family: monospace;
1910
- font-size: 14px;
1911
- box-shadow: 0 2px 4px rgba(0,0,0,0.2);
1912
- pointer-events: auto;
1913
- `;
1914
- banner.textContent = '🧪 TEST BROWSER - All console logs are being captured 📝';
1915
- shadow.appendChild(banner);
1916
- document.documentElement.appendChild(container);
1917
- }
1918
- // Wait for hydration to complete before injecting
1919
- if (typeof window !== 'undefined') {
1920
- // For Next.js apps, wait for hydration
1921
- const waitForHydration = () => {
1922
- let attempts = 0;
1923
- const checkInterval = setInterval(() => {
1924
- attempts++;
1925
- // Check various hydration indicators
1926
- const hydrated = window.__NEXT_HYDRATED ||
1927
- window._react_root ||
1928
- document.querySelector('[data-reactroot]') ||
1929
- document.querySelector('#__next') ||
1930
- attempts > 20; // Timeout after 5 seconds
1931
- if (hydrated) {
1932
- clearInterval(checkInterval);
1933
- // Add a small delay to be extra safe
1934
- setTimeout(injectTestBanner, 100);
1935
- }
1936
- }, 250);
1937
- };
1938
- // Start checking after DOMContentLoaded
1939
- if (document.readyState === 'loading') {
1940
- document.addEventListener('DOMContentLoaded', waitForHydration);
1941
- }
1942
- else {
1943
- waitForHydration();
1944
- }
1945
- }
1946
- });
1947
- // Set up console capture
1948
- page.on('console', (msg) => {
1949
- const timestamp = new Date().toISOString();
1950
- const msgType = msg.type().toUpperCase();
1951
- const msgText = msg.text();
1952
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType}] ${msgText}\n`;
1953
- appendFileSync(outputFile, logEntry);
1954
- // Flash red on errors
1955
- if (msgType === 'ERROR') {
1956
- page.evaluate(() => {
1957
- // @ts-ignore - browser context
1958
- const flash = document.createElement('div');
1959
- flash.style.cssText = `
1960
- position: fixed;
1961
- top: 0;
1962
- left: 0;
1963
- right: 0;
1964
- bottom: 0;
1965
- background: rgba(239, 68, 68, 0.3);
1966
- z-index: 999998;
1967
- pointer-events: none;
1968
- animation: errorFlash 0.5s ease-out;
1969
- `;
1970
- // @ts-ignore - browser context
1971
- const style = document.createElement('style');
1972
- style.textContent = `
1973
- @keyframes errorFlash {
1974
- 0% { opacity: 1; }
1975
- 100% { opacity: 0; }
1976
- }
1977
- `;
1978
- // @ts-ignore - browser context
1979
- document.head.appendChild(style);
1980
- // @ts-ignore - browser context
1981
- document.body.appendChild(flash);
1982
- setTimeout(() => flash.remove(), 500);
1983
- }).catch(() => { }); // Ignore errors from flashing
1984
- }
1985
- });
1986
- // Handle page errors (including hydration errors)
1987
- page.on('pageerror', (error) => {
1988
- const timestamp = new Date().toISOString();
1989
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] [PAGE ERROR] ${error.message}\n${error.stack}\n`;
1990
- appendFileSync(outputFile, logEntry);
1991
- // Visual feedback for page errors
1992
- page.evaluate(() => {
1993
- // @ts-ignore - browser context
1994
- const errorBanner = document.createElement('div');
1995
- errorBanner.style.cssText = `
1996
- position: fixed;
1997
- top: 40px;
1998
- left: 50%;
1999
- transform: translateX(-50%);
2000
- background: #dc2626;
2001
- color: white;
2002
- padding: 12px 24px;
2003
- border-radius: 6px;
2004
- font-family: monospace;
2005
- font-size: 14px;
2006
- z-index: 999999;
2007
- box-shadow: 0 4px 6px rgba(0,0,0,0.3);
2008
- animation: slideDown 0.3s ease-out;
2009
- `;
2010
- errorBanner.textContent = '⚠️ JavaScript Error - Check Console & Logs';
2011
- // @ts-ignore - browser context
2012
- const style = document.createElement('style');
2013
- style.textContent = `
2014
- @keyframes slideDown {
2015
- from { transform: translate(-50%, -100%); opacity: 0; }
2016
- to { transform: translate(-50%, 0); opacity: 1; }
2017
- }
2018
- `;
2019
- // @ts-ignore - browser context
2020
- document.head.appendChild(style);
2021
- // @ts-ignore - browser context
2022
- document.body.appendChild(errorBanner);
2023
- setTimeout(() => errorBanner.remove(), 5000);
2024
- }).catch(() => { });
2025
- });
2026
- // Handle unhandled promise rejections
2027
- await page.addInitScript(() => {
2028
- // @ts-ignore - browser context
2029
- window.addEventListener('unhandledrejection', (event) => {
2030
- console.error('Unhandled Promise Rejection:', event.reason);
2031
- });
2032
- });
2033
- // Handle network response errors
2034
- page.on('response', (response) => {
2035
- if (response.status() >= 400) {
2036
- const timestamp = new Date().toISOString();
2037
- const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NETWORK ERROR] ${response.status()} ${response.statusText()} - ${response.url()}\n`;
2038
- appendFileSync(outputFile, logEntry);
2039
- }
2040
- });
2041
- // Navigate to the app
2042
- const browserUrl = `http://localhost:${port}`;
2043
- await page.goto(browserUrl, { waitUntil: 'domcontentloaded' });
2044
- // Update server info with browser details
2045
- serverInfo.browser = browser;
2046
- serverInfo.page = page;
2047
- serverInfo.browserUrl = browserUrl;
2048
- serverInfo.consoleCapture = true;
2049
- // Log success
2050
- const successMessage = `
2051
- 🎯 TEST BROWSER LAUNCHED!
2052
-
2053
- 👉 Use the browser window that just opened (with green banner)
2054
- 📝 All console logs are being saved
2055
- 🔍 DevTools is open for debugging
2056
- ⚠️ Errors will flash red on screen
2057
-
2058
- Happy debugging! 🚀
2059
- `;
2060
- appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`);
2061
- appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ${successMessage}\n`);
2062
- return {
2063
- content: [
2064
- {
2065
- type: "text",
2066
- text: JSON.stringify({
2067
- status: "success",
2068
- message: successMessage.trim(),
2069
- processId: processId,
2070
- serverPid: devProcess.pid,
2071
- serverUrl: `http://localhost:${port}`,
2072
- logFile: outputFile,
2073
- browserStatus: "launched",
2074
- teachingMode: teachingMode
2075
- }, null, 2)
2076
- }
2077
- ]
2078
- };
2079
- }
2080
- catch (browserError) {
2081
- // Browser launch failed, but server is running
2082
- const errorMsg = browserError instanceof Error ? browserError.message : String(browserError);
2083
- appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [ERROR] Failed to launch browser: ${errorMsg}\n`);
2084
- return {
2085
- content: [
2086
- {
2087
- type: "text",
2088
- text: JSON.stringify({
2089
- status: "partial_success",
2090
- message: `Server started successfully on http://localhost:${port}, but browser launch failed`,
2091
- error: errorMsg,
2092
- processId: processId,
2093
- serverPid: devProcess.pid,
2094
- serverUrl: `http://localhost:${port}`,
2095
- logFile: outputFile,
2096
- browserStatus: "failed",
2097
- manualInstructions: "Open http://localhost:" + port + " in your browser manually"
2098
- }, null, 2)
2099
- }
2100
- ]
2101
- };
2102
- }
2103
- }
2104
- catch (error) {
2105
- throw new Error(`Failed to start frontend with browser: ${error instanceof Error ? error.message : String(error)}`);
2106
- }
2107
- }
2108
- default:
2109
- throw new Error(`Unknown tool: ${name}`);
2110
- }
270
+ case "dev_tail_sandbox_logs": {
271
+ const validatedArgs = TailSandboxLogsArgsSchema.parse(args);
272
+ const result = tailSandboxLogs(validatedArgs.lines);
273
+ return {
274
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
275
+ };
2111
276
  }
2112
- catch (error) {
2113
- const errorMessage = error instanceof Error ? error.message : String(error);
277
+ default:
2114
278
  return {
2115
- content: [
2116
- {
2117
- type: "text",
2118
- text: `Error: ${errorMessage}`
2119
- }
2120
- ],
279
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
2121
280
  isError: true
2122
281
  };
2123
- }
2124
- });
282
+ }
2125
283
  }
2126
- async run() {
2127
- const transport = new StdioServerTransport();
2128
- await this.server.connect(transport);
2129
- console.error("Dev Logger MCP server running on stdio");
284
+ catch (error) {
285
+ return {
286
+ content: [
287
+ {
288
+ type: "text",
289
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
290
+ }
291
+ ],
292
+ isError: true
293
+ };
294
+ }
295
+ });
296
+ // Cleanup on exit
297
+ process.on("SIGINT", () => {
298
+ if (sandboxProcess?.process) {
299
+ sandboxProcess.process.kill("SIGTERM");
300
+ }
301
+ process.exit(0);
302
+ });
303
+ process.on("SIGTERM", () => {
304
+ if (sandboxProcess?.process) {
305
+ sandboxProcess.process.kill("SIGTERM");
2130
306
  }
307
+ process.exit(0);
308
+ });
309
+ // Start the server
310
+ async function main() {
311
+ const transport = new StdioServerTransport();
312
+ await server.connect(transport);
313
+ console.error("MCP Dev Logger server running on stdio");
2131
314
  }
2132
- const server = new DevLoggerServer();
2133
- server.run().catch(console.error);
315
+ main().catch((error) => {
316
+ console.error("Fatal error in main():", error);
317
+ process.exit(1);
318
+ });
2134
319
  //# sourceMappingURL=index.js.map